tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 54from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74 75class TinkoffBrokerServer: 76 """ 77 This class implements methods to work with Tinkoff broker server. 78 79 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 80 81 About `token`: https://tinkoff.github.io/investAPI/token/ 82 """ 83 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 84 """ 85 Main class init. 86 87 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 88 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 89 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 90 :param useCache: use default cache file with raw data to use instead of `iList`. 91 True by default. Cache is auto-update if new day has come. 92 If you don't want to use cache and always updates raw data then set `useCache=False`. 93 :param defaultCache: path to default cache file. `dump.json` by default. 94 """ 95 if token is None or not token: 96 try: 97 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 98 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 99 100 except KeyError: 101 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 102 raise Exception("Token required") 103 104 else: 105 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 106 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 107 108 if accountId is None or not accountId: 109 try: 110 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 111 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 112 113 except KeyError: 114 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 115 116 else: 117 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 118 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 119 120 self.version = __version__ # duplicate here used TKSBrokerAPI main version 121 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 122 123 Latest version: https://pypi.org/project/tksbrokerapi/ 124 """ 125 126 self.aliases = TKS_TICKER_ALIASES 127 """Some aliases instead official tickers. 128 129 See also: `TKSEnums.TKS_TICKER_ALIASES` 130 """ 131 132 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 133 134 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 135 136 self._ticker = "" 137 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 138 139 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 140 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 141 142 See also: `SearchByTicker()`, `SearchInstruments()`. 143 """ 144 145 self._figi = "" 146 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 147 148 See also: `SearchByFIGI()`, `SearchInstruments()`. 149 """ 150 151 self.depth = 1 152 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 153 154 See also: `GetCurrentPrices()`. 155 """ 156 157 self.server = r"https://invest-public-api.tinkoff.ru/rest" 158 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 159 160 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 161 """ 162 163 uLogger.debug("Broker API server: {}".format(self.server)) 164 165 self.timeout = 15 166 """Server operations timeout in seconds. Default: `15`. 167 168 See also: `SendAPIRequest()`. 169 """ 170 171 self.headers = { 172 "Content-Type": "application/json", 173 "accept": "application/json", 174 "Authorization": "Bearer {}".format(self.token), 175 "x-app-name": "Tim55667757.TKSBrokerAPI", 176 } 177 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 178 179 See also: `SendAPIRequest()`. 180 """ 181 182 self.body = None 183 """Request body which send to broker server. Default: `None`. 184 185 See also: `SendAPIRequest()`. 186 """ 187 188 self.moreDebug = False 189 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 190 191 self.historyFile = None 192 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 193 194 See also: `History()`. 195 """ 196 197 self.htmlHistoryFile = "index.html" 198 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 199 200 See also: `ShowHistoryChart()`. 201 """ 202 203 self.instrumentsFile = "instruments.md" 204 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 205 206 See also: `ShowInstrumentsInfo()`. 207 """ 208 209 self.searchResultsFile = "search-results.md" 210 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 211 212 See also: `SearchInstruments()`. 213 """ 214 215 self.pricesFile = "prices.md" 216 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 217 218 See also: `GetListOfPrices()`. 219 """ 220 221 self.infoFile = "info.md" 222 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 223 224 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 225 """ 226 227 self.bondsXLSXFile = "ext-bonds.xlsx" 228 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 229 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 230 231 See also: `ExtendBondsData()`. 232 """ 233 234 self.calendarFile = "calendar.md" 235 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 236 237 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 238 239 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 240 """ 241 242 self.overviewFile = "overview.md" 243 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 244 245 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 246 """ 247 248 self.overviewDigestFile = "overview-digest.md" 249 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 250 251 See also: `Overview()` with parameter `details="digest"`. 252 """ 253 254 self.overviewPositionsFile = "overview-positions.md" 255 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 256 257 See also: `Overview()` with parameter `details="positions"`. 258 """ 259 260 self.overviewOrdersFile = "overview-orders.md" 261 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 262 263 See also: `Overview()` with parameter `details="orders"`. 264 """ 265 266 self.overviewAnalyticsFile = "overview-analytics.md" 267 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 268 269 See also: `Overview()` with parameter `details="analytics"`. 270 """ 271 272 self.overviewBondsCalendarFile = "overview-calendar.md" 273 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 274 275 See also: `Overview()` with parameter `details="calendar"`. 276 """ 277 278 self.reportFile = "deals.md" 279 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 280 281 See also: `Deals()`. 282 """ 283 284 self.withdrawalLimitsFile = "limits.md" 285 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 286 287 See also: `OverviewLimits()` and `RequestLimits()`. 288 """ 289 290 self.userInfoFile = "user-info.md" 291 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 292 293 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 294 """ 295 296 self.userAccountsFile = "accounts.md" 297 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 298 299 See also: `OverviewAccounts()`, `RequestAccounts()`. 300 """ 301 302 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 303 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 304 305 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 306 307 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 308 """ 309 310 self.iList = None # init iList for raw instruments data 311 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 312 313 See also: `Listing()`, `DumpInstruments()`. 314 """ 315 316 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 317 if useCache: 318 if os.path.exists(self.iListDumpFile): 319 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 320 curTime = datetime.now(tzutc()) 321 322 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 323 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 324 325 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 326 327 else: 328 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 329 330 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 331 os.path.abspath(self.iListDumpFile), 332 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 333 )) 334 335 else: 336 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 338 339 else: 340 self.iList = self.Listing() # request new raw instruments data from broker server 341 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 342 343 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 344 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 345 346 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 347 """ 348 349 @property 350 def ticker(self) -> str: 351 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 352 353 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 354 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 355 356 See also: `SearchByTicker()`, `SearchInstruments()`. 357 """ 358 return self._ticker 359 360 @ticker.setter 361 def ticker(self, value): 362 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 363 364 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 365 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 366 367 See also: `SearchByTicker()`, `SearchInstruments()`. 368 """ 369 self._ticker = str(value).upper() # Tickers may be upper case only 370 371 @property 372 def figi(self) -> str: 373 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 374 375 See also: `SearchByFIGI()`, `SearchInstruments()`. 376 """ 377 return self._figi 378 379 @figi.setter 380 def figi(self, value): 381 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 382 383 See also: `SearchByFIGI()`, `SearchInstruments()`. 384 """ 385 self._figi = str(value).upper() # FIGI may be upper case only 386 387 def _ParseJSON(self, rawData="{}") -> dict: 388 """ 389 Parse JSON from response string. 390 391 :param rawData: this is a string with JSON-formatted text. 392 :return: JSON (dictionary), parsed from server response string. 393 """ 394 responseJSON = json.loads(rawData) if rawData else {} 395 396 if self.moreDebug: 397 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 398 399 return responseJSON 400 401 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 402 """ 403 Send GET or POST request to broker server and receive JSON object. 404 405 self.header: must be defining with dictionary of headers. 406 self.body: if define then used as request body. None by default. 407 self.timeout: global request timeout, 15 seconds by default. 408 :param url: url with REST request. 409 :param reqType: send "GET" or "POST" request. "GET" by default. 410 :param retry: how many times retry after first request if an 5xx server errors occurred. 411 :param pause: sleep time in seconds between retries. 412 :return: response JSON (dictionary) from broker. 413 """ 414 if reqType.upper() not in ("GET", "POST"): 415 uLogger.error("You can define request type: `GET` or `POST`!") 416 raise Exception("Incorrect value") 417 418 if self.moreDebug: 419 uLogger.debug("Request parameters:") 420 uLogger.debug(" - REST API URL: {}".format(url)) 421 uLogger.debug(" - request type: {}".format(reqType)) 422 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 423 uLogger.debug(" - body:\n{}".format(self.body)) 424 425 # fast hack to avoid all operations with some tickers/FIGI 426 responseJSON = {} 427 oK = True 428 for item in self.exclude: 429 if item in url: 430 if self.moreDebug: 431 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 432 433 oK = False 434 break 435 436 if oK: 437 counter = 0 438 response = None 439 errMsg = "" 440 441 while not response and counter <= retry: 442 if reqType == "GET": 443 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 444 445 if reqType == "POST": 446 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 447 448 if self.moreDebug: 449 uLogger.debug("Response:") 450 uLogger.debug(" - status code: {}".format(response.status_code)) 451 uLogger.debug(" - reason: {}".format(response.reason)) 452 uLogger.debug(" - body length: {}".format(len(response.text))) 453 uLogger.debug(" - headers:\n{}".format(response.headers)) 454 455 # Server returns some headers: 456 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 457 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 458 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 459 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 460 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 461 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 462 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 463 sleep(rateLimitWait) 464 465 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 466 if 400 <= response.status_code < 500: 467 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 468 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 469 470 if "code" in response.text and "message" in response.text: 471 msgDict = self._ParseJSON(rawData=response.text) 472 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 473 474 counter = retry + 1 # do not retry for 4xx errors 475 476 if 500 <= response.status_code < 600: 477 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 478 uLogger.debug(" - not oK, {}".format(errMsg)) 479 480 if "code" in response.text and "message" in response.text: 481 errMsgDict = self._ParseJSON(rawData=response.text) 482 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 483 484 counter += 1 485 486 if counter <= retry: 487 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 488 sleep(pause) 489 490 responseJSON = self._ParseJSON(rawData=response.text) 491 492 if errMsg: 493 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 494 uLogger.error(" - not oK, {}".format(errMsg)) 495 496 return responseJSON 497 498 def _IUpdater(self, iType: str) -> tuple: 499 """ 500 Request instrument by type from server. See available API methods for instruments: 501 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 502 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 503 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 504 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 505 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 506 507 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 508 :return: tuple with iType name and list of available instruments of current type for defined user token. 509 """ 510 result = [] 511 512 if iType in TKS_INSTRUMENTS: 513 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 514 515 # all instruments have the same body in API v2 requests: 516 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 517 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 518 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 519 520 return iType, result 521 522 def _IWrapper(self, kwargs): 523 """ 524 Wrapper runs instrument's update method `_IUpdater()`. 525 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 526 """ 527 return self._IUpdater(**kwargs) 528 529 def Listing(self) -> dict: 530 """ 531 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 532 533 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 534 """ 535 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 536 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 537 538 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 539 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 540 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 541 542 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 543 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 544 poolUpdater.close() 545 546 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 547 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 548 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 549 550 # calculate minimum price increment (step) for all instruments and set up instrument's type: 551 for iType in iList.keys(): 552 for ticker in iList[iType]: 553 iList[iType][ticker]["type"] = iType 554 555 if "minPriceIncrement" in iList[iType][ticker].keys(): 556 iList[iType][ticker]["step"] = NanoToFloat( 557 iList[iType][ticker]["minPriceIncrement"]["units"], 558 iList[iType][ticker]["minPriceIncrement"]["nano"], 559 ) 560 561 else: 562 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 563 564 return iList 565 566 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 567 """ 568 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 569 570 See also: `DumpInstruments()`, `Listing()`. 571 572 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 573 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 574 """ 575 if self.iListDumpFile is None or not self.iListDumpFile: 576 uLogger.error("Output name of dump file must be defined!") 577 raise Exception("Filename required") 578 579 if not self.iList or forceUpdate: 580 self.iList = self.Listing() 581 582 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 583 584 # Save as XLSX with separated sheets for every type of instruments: 585 with pd.ExcelWriter( 586 path=xlsxDumpFile, 587 date_format=TKS_DATE_FORMAT, 588 datetime_format=TKS_DATE_TIME_FORMAT, 589 mode="w", 590 ) as writer: 591 for iType in TKS_INSTRUMENTS: 592 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 593 df = df[sorted(df)] # sorted by column names 594 df = df.applymap( 595 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 596 na_action="ignore", 597 ) # converting numbers from nano-type to float in every cell 598 df.to_excel( 599 writer, 600 sheet_name=iType, 601 encoding="UTF-8", 602 freeze_panes=(1, 1), 603 ) # saving as XLSX-file with freeze first row and column as headers 604 605 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 606 607 def DumpInstruments(self, forceUpdate: bool = True) -> str: 608 """ 609 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 610 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 611 612 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 613 614 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 615 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 616 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 617 """ 618 if self.iListDumpFile is None or not self.iListDumpFile: 619 uLogger.error("Output name of dump file must be defined!") 620 raise Exception("Filename required") 621 622 if not self.iList or forceUpdate: 623 self.iList = self.Listing() 624 625 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 626 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 627 fH.write(jsonDump) 628 629 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 630 631 return jsonDump 632 633 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 634 """ 635 Show information about one instrument defined by json data and prints it in Markdown format. 636 637 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 638 639 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 640 :param show: if `True` then also printing information about instrument and its current price. 641 :return: multilines text in Markdown format with information about one instrument. 642 """ 643 splitLine = "| | |\n" 644 infoText = "" 645 646 if iJSON is not None and iJSON and isinstance(iJSON, dict): 647 info = [ 648 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 649 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 650 "| Parameters | Values |\n", 651 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 652 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 653 "| Full name: | {:<54} |\n".format(iJSON["name"]), 654 ] 655 656 if "sector" in iJSON.keys() and iJSON["sector"]: 657 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 658 659 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 660 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 661 662 info.extend([ 663 splitLine, 664 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 665 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 666 ]) 667 668 if "isin" in iJSON.keys() and iJSON["isin"]: 669 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 670 671 if "classCode" in iJSON.keys(): 672 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 673 674 info.extend([ 675 splitLine, 676 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 677 splitLine, 678 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 679 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 680 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 681 ]) 682 683 if iJSON["figi"]: 684 self._figi = iJSON["figi"] 685 iJSON = iJSON | self.RequestTradingStatus() 686 687 info.extend([ 688 splitLine, 689 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 690 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 691 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 692 ]) 693 694 info.append(splitLine) 695 696 if "type" in iJSON.keys() and iJSON["type"]: 697 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 698 699 if "shareType" in iJSON.keys() and iJSON["shareType"]: 700 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 701 702 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 703 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 704 705 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 706 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 707 708 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 709 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 710 711 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 712 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 713 714 if "focusType" in iJSON.keys() and iJSON["focusType"]: 715 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 716 717 if "assetType" in iJSON.keys() and iJSON["assetType"]: 718 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 719 720 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 721 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 722 723 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 724 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 725 726 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 727 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 728 729 if "currency" in iJSON.keys(): 730 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 731 732 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 733 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 734 735 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 736 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 737 738 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 739 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 740 741 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 742 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 743 744 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 745 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 746 747 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 748 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 749 750 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 751 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 752 753 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 754 info.append("| Perpetual bond: | Yes |\n") 755 756 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 757 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 758 759 iExt = None 760 if iJSON["type"] == "Bonds": 761 info.extend([ 762 splitLine, 763 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 764 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 765 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 766 iJSON["nominal"]["currency"], 767 )), 768 ]) 769 770 if "floatingCouponFlag" in iJSON.keys(): 771 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 772 773 if "amortizationFlag" in iJSON.keys(): 774 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 775 776 info.append(splitLine) 777 778 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 779 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 780 781 if iJSON["figi"]: 782 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 783 784 info.extend([ 785 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 786 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 787 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 788 ]) 789 790 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 791 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 792 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 793 iJSON["aciValue"]["currency"] 794 ))) 795 796 if "currentPrice" in iJSON.keys(): 797 info.append(splitLine) 798 799 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 800 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 801 802 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 803 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 804 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 805 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 806 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 807 808 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 809 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 810 811 info.extend([ 812 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 813 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 814 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 815 )), 816 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 817 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 818 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 819 )), 820 "| Changes between last deal price and last close | {:<54} |\n".format( 821 "{:.2f}%{}".format( 822 iJSON["currentPrice"]["changes"], 823 " ({}{:.2f} {})".format( 824 "+" if bondChangesDelta > 0 else "", 825 bondChangesDelta, 826 aciCurrency 827 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 828 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 829 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 830 currency 831 ), 832 ) 833 ), 834 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 835 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 836 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 837 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 838 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 839 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 840 )), 841 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 842 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 843 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 844 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 845 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 846 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 847 )), 848 ]) 849 850 if "lot" in iJSON.keys(): 851 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 852 853 if "step" in iJSON.keys() and iJSON["step"] != 0: 854 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 855 856 # Add bond payment calendar: 857 if iJSON["type"] == "Bonds": 858 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 859 info.extend(["\n", strCalendar]) 860 861 infoText += "".join(info) 862 863 if show: 864 uLogger.info("{}".format(infoText)) 865 866 else: 867 uLogger.debug("{}".format(infoText)) 868 869 if self.infoFile is not None: 870 with open(self.infoFile, "w", encoding="UTF-8") as fH: 871 fH.write(infoText) 872 873 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 874 875 return infoText 876 877 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 878 """ 879 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 880 881 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 882 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 883 :return: JSON formatted data with information about instrument. 884 """ 885 tickerJSON = {} 886 if self.moreDebug: 887 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 888 889 if not self._ticker: 890 uLogger.warning("self._ticker variable is not be empty!") 891 892 else: 893 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 894 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 895 raise Exception("Instrument not allowed") 896 897 if not self.iList: 898 self.iList = self.Listing() 899 900 if self._ticker in self.iList["Shares"].keys(): 901 tickerJSON = self.iList["Shares"][self._ticker] 902 if self.moreDebug: 903 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 904 905 elif self._ticker in self.iList["Currencies"].keys(): 906 tickerJSON = self.iList["Currencies"][self._ticker] 907 if self.moreDebug: 908 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 909 910 elif self._ticker in self.iList["Bonds"].keys(): 911 tickerJSON = self.iList["Bonds"][self._ticker] 912 if self.moreDebug: 913 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 914 915 elif self._ticker in self.iList["Etfs"].keys(): 916 tickerJSON = self.iList["Etfs"][self._ticker] 917 if self.moreDebug: 918 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 919 920 elif self._ticker in self.iList["Futures"].keys(): 921 tickerJSON = self.iList["Futures"][self._ticker] 922 if self.moreDebug: 923 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 924 925 if tickerJSON: 926 self._figi = tickerJSON["figi"] 927 928 if requestPrice: 929 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 930 931 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 932 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 933 934 else: 935 tickerJSON["currentPrice"]["changes"] = 0 936 937 if show: 938 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 939 940 else: 941 if show: 942 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 943 944 return tickerJSON 945 946 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 947 """ 948 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 949 950 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 951 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 952 :return: JSON formatted data with information about instrument. 953 """ 954 figiJSON = {} 955 if self.moreDebug: 956 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 957 958 if not self._figi: 959 uLogger.warning("self._figi variable is not be empty!") 960 961 else: 962 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 963 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 964 raise Exception("Instrument not allowed") 965 966 if not self.iList: 967 self.iList = self.Listing() 968 969 for item in self.iList["Shares"].keys(): 970 if self._figi == self.iList["Shares"][item]["figi"]: 971 figiJSON = self.iList["Shares"][item] 972 973 if self.moreDebug: 974 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 975 976 break 977 978 if not figiJSON: 979 for item in self.iList["Currencies"].keys(): 980 if self._figi == self.iList["Currencies"][item]["figi"]: 981 figiJSON = self.iList["Currencies"][item] 982 983 if self.moreDebug: 984 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 985 986 break 987 988 if not figiJSON: 989 for item in self.iList["Bonds"].keys(): 990 if self._figi == self.iList["Bonds"][item]["figi"]: 991 figiJSON = self.iList["Bonds"][item] 992 993 if self.moreDebug: 994 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 995 996 break 997 998 if not figiJSON: 999 for item in self.iList["Etfs"].keys(): 1000 if self._figi == self.iList["Etfs"][item]["figi"]: 1001 figiJSON = self.iList["Etfs"][item] 1002 1003 if self.moreDebug: 1004 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1005 1006 break 1007 1008 if not figiJSON: 1009 for item in self.iList["Futures"].keys(): 1010 if self._figi == self.iList["Futures"][item]["figi"]: 1011 figiJSON = self.iList["Futures"][item] 1012 1013 if self.moreDebug: 1014 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1015 1016 break 1017 1018 if figiJSON: 1019 self._figi = figiJSON["figi"] 1020 self._ticker = figiJSON["ticker"] 1021 1022 if requestPrice: 1023 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1024 1025 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1026 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1027 1028 else: 1029 figiJSON["currentPrice"]["changes"] = 0 1030 1031 if show: 1032 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1033 1034 else: 1035 if show: 1036 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1037 1038 return figiJSON 1039 1040 def GetCurrentPrices(self, show: bool = True) -> dict: 1041 """ 1042 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1043 `{"buy": [{"price": 1243.8, "quantity": 193}, 1044 {"price": 1244.0, "quantity": 168}, 1045 {"price": 1244.8, "quantity": 5}, 1046 {"price": 1245.0, "quantity": 61}, 1047 {"price": 1245.4, "quantity": 60}], 1048 "sell": [{"price": 1243.6, "quantity": 8}, 1049 {"price": 1242.6, "quantity": 10}, 1050 {"price": 1242.4, "quantity": 18}, 1051 {"price": 1242.2, "quantity": 50}, 1052 {"price": 1242.0, "quantity": 113}], 1053 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1054 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1055 - sell: list of dicts with Buyers prices, 1056 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1057 - quantity: volume value by current price in lots, 1058 - limitUp: current trade session limit price, maximum, 1059 - limitDown: current trade session limit price, minimum, 1060 - lastPrice: last deal price of the instrument, 1061 - closePrice: previous trade session close price of the instrument. 1062 1063 See also: `SearchByTicker()` and `SearchByFIGI()`. 1064 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1065 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1066 1067 :param show: if `True` then print DOM to log and console. 1068 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1069 If an error occurred then returns an empty record: 1070 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1071 """ 1072 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1073 1074 if self.depth < 1: 1075 uLogger.error("Depth of Market (DOM) must be >=1!") 1076 raise Exception("Incorrect value") 1077 1078 if not (self._ticker or self._figi): 1079 uLogger.error("self._ticker or self._figi variables must be defined!") 1080 raise Exception("Ticker or FIGI required") 1081 1082 if self._ticker and not self._figi: 1083 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1084 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1085 1086 if not self._ticker and self._figi: 1087 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1088 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1089 1090 if not self._figi: 1091 uLogger.error("FIGI is not defined!") 1092 raise Exception("Ticker or FIGI required") 1093 1094 else: 1095 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1096 1097 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1098 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1099 self.body = str({"figi": self._figi, "depth": self.depth}) 1100 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1101 1102 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1103 # list of dicts with sellers orders: 1104 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1105 1106 # list of dicts with buyers orders: 1107 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1108 1109 # max price of instrument at this time: 1110 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1111 1112 # min price of instrument at this time: 1113 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1114 1115 # last price of deal with instrument: 1116 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1117 1118 # last close price of instrument: 1119 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1120 1121 else: 1122 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1123 uLogger.debug("Server response: {}".format(pricesResponse)) 1124 1125 if show: 1126 if prices["buy"] or prices["sell"]: 1127 info = [ 1128 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1129 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1130 self._ticker, 1131 self._figi, 1132 self.depth, 1133 ), 1134 "-" * 60, "\n", 1135 " Orders of Buyers | Orders of Sellers\n", 1136 "-" * 60, "\n", 1137 " Sell prices (volumes) | Buy prices (volumes)\n", 1138 "-" * 60, "\n", 1139 ] 1140 1141 if not prices["buy"]: 1142 info.append(" | No orders!\n") 1143 sumBuy = 0 1144 1145 else: 1146 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1147 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1148 for item in maxMinSorted: 1149 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1150 1151 if not prices["sell"]: 1152 info.append("No orders! |\n") 1153 sumSell = 0 1154 1155 else: 1156 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1157 for item in prices["sell"]: 1158 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1159 1160 info.extend([ 1161 "-" * 60, "\n", 1162 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1163 "-" * 60, "\n", 1164 ]) 1165 1166 infoText = "".join(info) 1167 1168 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1169 1170 else: 1171 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1172 1173 return prices 1174 1175 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1176 """ 1177 This method get and show information about all available broker instruments for current user account. 1178 If `instrumentsFile` string is not empty then also save information to this file. 1179 1180 :param show: if `True` then print results to console, if `False` — print only to file. 1181 :return: multi-lines string with all available broker instruments 1182 """ 1183 if not self.iList: 1184 self.iList = self.Listing() 1185 1186 info = [ 1187 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1188 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1189 ] 1190 1191 # add instruments count by type: 1192 for iType in self.iList.keys(): 1193 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1194 1195 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1196 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1197 1198 # generating info tables with all instruments by type: 1199 for iType in self.iList.keys(): 1200 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1201 1202 for instrument in self.iList[iType].keys(): 1203 iName = self.iList[iType][instrument]["name"] # instrument's name 1204 if len(iName) > 57: 1205 iName = "{}...".format(iName[:54]) # right trim for a long string 1206 1207 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1208 self.iList[iType][instrument]["ticker"], 1209 iName, 1210 self.iList[iType][instrument]["figi"], 1211 self.iList[iType][instrument]["currency"], 1212 self.iList[iType][instrument]["lot"], 1213 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1214 )) 1215 1216 infoText = "".join(info) 1217 1218 if show: 1219 uLogger.info(infoText) 1220 1221 if self.instrumentsFile: 1222 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1223 fH.write(infoText) 1224 1225 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1226 1227 return infoText 1228 1229 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1230 """ 1231 This method search and show information about instruments by part of its ticker, FIGI or name. 1232 If `searchResultsFile` string is not empty then also save information to this file. 1233 1234 :param pattern: string with part of ticker, FIGI or instrument's name. 1235 :param show: if `True` then print results to console, if `False` — return list of result only. 1236 :return: list of dictionaries with all found instruments. 1237 """ 1238 if not self.iList: 1239 self.iList = self.Listing() 1240 1241 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1242 compiledPattern = re.compile(pattern, re.IGNORECASE) 1243 1244 for iType in self.iList: 1245 for instrument in self.iList[iType].values(): 1246 searchResult = compiledPattern.search(" ".join( 1247 [instrument["ticker"], instrument["figi"], instrument["name"]] 1248 )) 1249 1250 if searchResult: 1251 searchResults[iType][instrument["ticker"]] = instrument 1252 1253 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1254 info = [ 1255 "# Search results\n\n", 1256 "* **Search pattern:** [{}]\n".format(pattern), 1257 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1258 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1259 ] 1260 infoShort = info[:] 1261 1262 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1263 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1264 skippedLine = "| ... | ... | ... | ... |\n" 1265 1266 if resultsLen == 0: 1267 info.append("\nNo results\n") 1268 infoShort.append("\nNo results\n") 1269 uLogger.warning("No results. Try changing your search pattern.") 1270 1271 else: 1272 for iType in searchResults: 1273 iTypeValuesCount = len(searchResults[iType].values()) 1274 if iTypeValuesCount > 0: 1275 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1276 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1277 1278 for instrument in searchResults[iType].values(): 1279 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1280 instrument["type"], 1281 instrument["ticker"], 1282 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1283 instrument["figi"], 1284 )) 1285 1286 if iTypeValuesCount <= 5: 1287 infoShort.extend(info[-iTypeValuesCount:]) 1288 1289 else: 1290 infoShort.extend(info[-5:]) 1291 infoShort.append(skippedLine) 1292 1293 infoText = "".join(info) 1294 infoTextShort = "".join(infoShort) 1295 1296 if show: 1297 uLogger.info(infoTextShort) 1298 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1299 1300 if self.searchResultsFile: 1301 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1302 fH.write(infoText) 1303 1304 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1305 1306 return searchResults 1307 1308 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1309 """ 1310 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1311 1312 :param instruments: list of strings with tickers or FIGIs. 1313 :return: list with unique instrument FIGIs only. 1314 """ 1315 requestedInstruments = [] 1316 for iName in instruments: 1317 if iName not in self.aliases.keys(): 1318 if iName not in requestedInstruments: 1319 requestedInstruments.append(iName) 1320 1321 else: 1322 if iName not in requestedInstruments: 1323 if self.aliases[iName] not in requestedInstruments: 1324 requestedInstruments.append(self.aliases[iName]) 1325 1326 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1327 1328 onlyUniqueFIGIs = [] 1329 for iName in requestedInstruments: 1330 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1331 continue 1332 1333 self._ticker = iName 1334 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1335 1336 if not iData: 1337 self._ticker = "" 1338 self._figi = iName 1339 1340 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1341 1342 if not iData: 1343 self._figi = "" 1344 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1345 1346 if iData and iData["figi"] not in onlyUniqueFIGIs: 1347 onlyUniqueFIGIs.append(iData["figi"]) 1348 1349 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1350 1351 return onlyUniqueFIGIs 1352 1353 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1354 """ 1355 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1356 1357 See limits: https://tinkoff.github.io/investAPI/limits/ 1358 1359 If `pricesFile` string is not empty then also save information to this file. 1360 1361 :param instruments: list of strings with tickers or FIGIs. 1362 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1363 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1364 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1365 """ 1366 if instruments is None or not instruments: 1367 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1368 raise Exception("Ticker or FIGI required") 1369 1370 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1371 1372 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1373 1374 iList = [] # trying to get info and current prices about all unique instruments: 1375 for self._figi in onlyUniqueFIGIs: 1376 iData = self.SearchByFIGI(requestPrice=True) 1377 iList.append(iData) 1378 1379 self.ShowListOfPrices(iList, show) 1380 1381 return iList 1382 1383 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1384 """ 1385 Show table contains current prices of given instruments. 1386 1387 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1388 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1389 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1390 :return: multilines text in Markdown format as a table contains current prices. 1391 """ 1392 infoText = "" 1393 1394 if show or self.pricesFile: 1395 info = [ 1396 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1397 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1398 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1399 ] 1400 1401 for item in iList: 1402 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1403 item["ticker"], 1404 item["figi"], 1405 item["type"], 1406 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1407 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1408 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1409 "{} / {}".format( 1410 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1411 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1412 ), 1413 "{} / {}".format( 1414 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1415 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1416 ), 1417 item["currency"], 1418 )) 1419 1420 infoText = "".join(info) 1421 1422 if show: 1423 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1424 1425 if self.pricesFile: 1426 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1427 fH.write(infoText) 1428 1429 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1430 1431 return infoText 1432 1433 def RequestTradingStatus(self) -> dict: 1434 """ 1435 Requesting trading status for the instrument defined by `figi` variable. 1436 1437 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1438 1439 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1440 1441 :return: dictionary with trading status attributes. Response example: 1442 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1443 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1444 """ 1445 if self._figi is None or not self._figi: 1446 uLogger.error("Variable `figi` must be defined for using this method!") 1447 raise Exception("FIGI required") 1448 1449 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1450 1451 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1452 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1453 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1454 1455 if self.moreDebug: 1456 uLogger.debug("Records about current trading status successfully received") 1457 1458 return tradingStatus 1459 1460 def RequestPortfolio(self) -> dict: 1461 """ 1462 Requesting actual user's portfolio for current `accountId`. 1463 1464 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1465 1466 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1467 1468 :return: dictionary with user's portfolio. 1469 """ 1470 if self.accountId is None or not self.accountId: 1471 uLogger.error("Variable `accountId` must be defined for using this method!") 1472 raise Exception("Account ID required") 1473 1474 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1475 1476 self.body = str({"accountId": self.accountId}) 1477 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1478 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1479 1480 if self.moreDebug: 1481 uLogger.debug("Records about user's portfolio successfully received") 1482 1483 return rawPortfolio 1484 1485 def RequestPositions(self) -> dict: 1486 """ 1487 Requesting open positions by currencies and instruments for current `accountId`. 1488 1489 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1490 1491 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1492 1493 :return: dictionary with open positions by instruments. 1494 """ 1495 if self.accountId is None or not self.accountId: 1496 uLogger.error("Variable `accountId` must be defined for using this method!") 1497 raise Exception("Account ID required") 1498 1499 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1500 1501 self.body = str({"accountId": self.accountId}) 1502 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1503 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1504 1505 if self.moreDebug: 1506 uLogger.debug("Records about current open positions successfully received") 1507 1508 return rawPositions 1509 1510 def RequestPendingOrders(self) -> list: 1511 """ 1512 Requesting current actual pending limit orders for current `accountId`. 1513 1514 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1515 1516 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1517 1518 :return: list of dictionaries with pending limit orders. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1528 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1529 1530 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1531 1532 return rawOrders 1533 1534 def RequestStopOrders(self) -> list: 1535 """ 1536 Requesting current actual stop orders for current `accountId`. 1537 1538 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1539 1540 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1541 1542 :return: list of dictionaries with stop orders. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1552 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1553 1554 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1555 1556 return rawStopOrders 1557 1558 def Overview(self, show: bool = False, details: str = "full") -> dict: 1559 """ 1560 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1561 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1562 and `overviewBondsCalendarFile` are defined then also save information to file. 1563 1564 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1565 many requests about the state of the portfolio, and then, based on the received data, a large number 1566 of calculation and statistics are collected. 1567 1568 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1569 :param details: how detailed should the information be? 1570 - `full` — shows full available information about portfolio status (by default), 1571 - `positions` — shows only open positions, 1572 - `orders` — shows only sections of open limits and stop orders. 1573 - `digest` — show a short digest of the portfolio status, 1574 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1575 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1576 :return: dictionary with client's raw portfolio and some statistics. 1577 """ 1578 if self.accountId is None or not self.accountId: 1579 uLogger.error("Variable `accountId` must be defined for using this method!") 1580 raise Exception("Account ID required") 1581 1582 view = { 1583 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1584 "headers": {}, # list of dictionaries, response headers without "positions" section 1585 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1586 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1587 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1588 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1589 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1590 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1591 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1592 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1593 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1594 }, 1595 "stat": { # --- some statistics calculated using "raw" sections: 1596 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1597 "availableRUB": 0., # available rubles (without other currencies) 1598 "blockedRUB": 0., # blocked sum in Russian Rouble 1599 "totalChangesRUB": 0., # changes for all open trades in RUB 1600 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1601 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1602 "sharesCostRUB": 0., # costs of all shares in RUB 1603 "bondsCostRUB": 0., # costs of all bonds in RUB 1604 "etfsCostRUB": 0., # costs of all etfs in RUB 1605 "futuresCostRUB": 0., # costs of all futures in RUB 1606 "Currencies": [], # list of dictionaries of all currencies statistics 1607 "Shares": [], # list of dictionaries of all shares statistics 1608 "Bonds": [], # list of dictionaries of all bonds statistics 1609 "Etfs": [], # list of dictionaries of all etfs statistics 1610 "Futures": [], # list of dictionaries of all futures statistics 1611 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1612 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1613 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1614 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1615 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1616 }, 1617 "analytics": { # --- some analytics of portfolio: 1618 "distrByAssets": {}, # portfolio distribution by assets 1619 "distrByCompanies": {}, # portfolio distribution by companies 1620 "distrBySectors": {}, # portfolio distribution by sectors 1621 "distrByCurrencies": {}, # portfolio distribution by currencies 1622 "distrByCountries": {}, # portfolio distribution by countries 1623 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1624 } 1625 } 1626 1627 details = details.lower() 1628 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1629 if details not in availableDetails: 1630 details = "full" 1631 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1632 1633 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1634 1635 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1636 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1637 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1638 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1639 1640 # save response headers without "positions" section: 1641 for key in portfolioResponse.keys(): 1642 if key != "positions": 1643 view["raw"]["headers"][key] = portfolioResponse[key] 1644 1645 else: 1646 continue 1647 1648 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1649 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1650 for item in portfolioResponse["positions"]: 1651 if item["instrumentType"] == "currency": 1652 self._figi = item["figi"] 1653 curr = self.SearchByFIGI(requestPrice=False) 1654 1655 # current price of currency in RUB: 1656 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1657 "name": curr["name"], 1658 "currentPrice": NanoToFloat( 1659 item["currentPrice"]["units"], 1660 item["currentPrice"]["nano"] 1661 ), 1662 } 1663 1664 view["raw"]["Currencies"].append(item) 1665 1666 elif item["instrumentType"] == "share": 1667 view["raw"]["Shares"].append(item) 1668 1669 elif item["instrumentType"] == "bond": 1670 view["raw"]["Bonds"].append(item) 1671 1672 elif item["instrumentType"] == "etf": 1673 view["raw"]["Etfs"].append(item) 1674 1675 elif item["instrumentType"] == "futures": 1676 view["raw"]["Futures"].append(item) 1677 1678 else: 1679 continue 1680 1681 # how many volume of currencies (by ISO currency name) are blocked: 1682 for item in view["raw"]["positions"]["blocked"]: 1683 blocked = NanoToFloat(item["units"], item["nano"]) 1684 if blocked > 0: 1685 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1686 1687 # how many volume of instruments (by FIGI) are blocked: 1688 for item in view["raw"]["positions"]["securities"]: 1689 blocked = int(item["blocked"]) 1690 if blocked > 0: 1691 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1692 1693 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1694 1695 if "rub" in allBlocked.keys(): 1696 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1697 1698 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1699 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1700 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1701 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1702 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1703 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1704 view["stat"]["portfolioCostRUB"] = sum([ 1705 view["stat"]["allCurrenciesCostRUB"], 1706 view["stat"]["sharesCostRUB"], 1707 view["stat"]["bondsCostRUB"], 1708 view["stat"]["etfsCostRUB"], 1709 view["stat"]["futuresCostRUB"], 1710 ]) 1711 1712 # --- calculating some portfolio statistics: 1713 byComp = {} # distribution by companies 1714 bySect = {} # distribution by sectors 1715 byCurr = {} # distribution by currencies (include RUB) 1716 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1717 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1718 1719 for item in portfolioResponse["positions"]: 1720 self._figi = item["figi"] 1721 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1722 1723 if instrument: 1724 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1725 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1726 1727 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1728 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1729 1730 else: 1731 blocked = 0 1732 1733 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1734 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1735 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1736 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1737 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1738 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1739 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1740 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1741 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1742 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1743 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1744 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1745 1746 statData = { 1747 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1748 "ticker": instrument["ticker"], # ticker by FIGI 1749 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1750 "volume": volume, # available volume of instrument 1751 "lots": lots, # volume in lots of instrument 1752 "direction": direction, # direction of an instrument's position: short or long 1753 "blocked": blocked, # blocked volume of currency or instrument 1754 "currentPrice": curPrice, # current instrument's price in basic asset 1755 "average": average, # current average position price 1756 "cost": cost, # current cost of all volume of instrument in basic asset 1757 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1758 "costRUB": costRUB, # cost of instrument in ruble 1759 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1760 "profit": profit, # expected profit at current moment 1761 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1762 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1763 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1764 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1765 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1766 "step": instrument["step"], # minimum price increment 1767 } 1768 1769 # adding distribution by unique countries: 1770 if statData["country"] not in byCountry.keys(): 1771 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1772 1773 else: 1774 byCountry[statData["country"]]["cost"] += costRUB 1775 byCountry[statData["country"]]["percent"] += percentCostRUB 1776 1777 if item["instrumentType"] != "currency": 1778 # adding distribution by unique companies: 1779 if statData["name"]: 1780 if statData["name"] not in byComp.keys(): 1781 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1782 1783 else: 1784 byComp[statData["name"]]["cost"] += costRUB 1785 byComp[statData["name"]]["percent"] += percentCostRUB 1786 1787 # adding distribution by unique sectors: 1788 if statData["sector"] not in bySect.keys(): 1789 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1790 1791 else: 1792 bySect[statData["sector"]]["cost"] += costRUB 1793 bySect[statData["sector"]]["percent"] += percentCostRUB 1794 1795 # adding distribution by unique currencies: 1796 if currency not in byCurr.keys(): 1797 byCurr[currency] = { 1798 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1799 "cost": costRUB, 1800 "percent": percentCostRUB 1801 } 1802 1803 else: 1804 byCurr[currency]["cost"] += costRUB 1805 byCurr[currency]["percent"] += percentCostRUB 1806 1807 # saving statistics for every instrument: 1808 if item["instrumentType"] == "currency": 1809 view["stat"]["Currencies"].append(statData) 1810 1811 # update dict with free funds for trading (total - blocked) by currencies 1812 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1813 view["stat"]["funds"][currency] = { 1814 "total": volume, 1815 "totalCostRUB": costRUB, # total volume cost in rubles 1816 "free": volume - blocked, 1817 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1818 } 1819 1820 elif item["instrumentType"] == "share": 1821 view["stat"]["Shares"].append(statData) 1822 1823 elif item["instrumentType"] == "bond": 1824 view["stat"]["Bonds"].append(statData) 1825 1826 elif item["instrumentType"] == "etf": 1827 view["stat"]["Etfs"].append(statData) 1828 1829 elif item["instrumentType"] == "Futures": 1830 view["stat"]["Futures"].append(statData) 1831 1832 else: 1833 continue 1834 1835 # total changes in Russian Ruble: 1836 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1837 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1838 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1839 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1840 view["stat"]["funds"]["rub"] = { 1841 "total": view["stat"]["availableRUB"], 1842 "totalCostRUB": view["stat"]["availableRUB"], 1843 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1844 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1845 } 1846 1847 # --- pending limit orders sector data: 1848 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1849 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1850 1851 for item in view["raw"]["orders"]: 1852 self._figi = item["figi"] 1853 1854 if item["figi"] not in uniquePendingOrdersFIGIs: 1855 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1856 1857 uniquePendingOrdersFIGIs.append(item["figi"]) 1858 uniquePendingOrders[item["figi"]] = instrument 1859 1860 else: 1861 instrument = uniquePendingOrders[item["figi"]] 1862 1863 if instrument: 1864 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1865 orderType = TKS_ORDER_TYPES[item["orderType"]] 1866 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1867 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1868 1869 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1870 if item["direction"] == "ORDER_DIRECTION_BUY": 1871 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1872 1873 else: 1874 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1875 1876 # requested price for order execution: 1877 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1878 1879 # necessary changes in percent to reach target from current price: 1880 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1881 1882 view["stat"]["orders"].append({ 1883 "orderID": item["orderId"], # orderId number parameter of current order 1884 "figi": item["figi"], # FIGI identification 1885 "ticker": instrument["ticker"], # ticker name by FIGI 1886 "lotsRequested": item["lotsRequested"], # requested lots value 1887 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1888 "currentPrice": lastPrice, # current instrument's price for defined action 1889 "targetPrice": target, # requested price for order execution in base currency 1890 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1891 "percentChanges": changes, # changes in percent to target from current price 1892 "currency": item["currency"], # instrument's currency name 1893 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1894 "type": orderType, # type of order from TKS_ORDER_TYPES 1895 "status": orderState, # order status from TKS_ORDER_STATES 1896 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1897 }) 1898 1899 # --- stop orders sector data: 1900 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1901 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1902 1903 for item in view["raw"]["stopOrders"]: 1904 self._figi = item["figi"] 1905 1906 if item["figi"] not in uniqueStopOrdersFIGIs: 1907 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1908 1909 uniqueStopOrdersFIGIs.append(item["figi"]) 1910 uniqueStopOrders[item["figi"]] = instrument 1911 1912 else: 1913 instrument = uniqueStopOrders[item["figi"]] 1914 1915 if instrument: 1916 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1917 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1918 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1919 1920 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1921 if "expirationTime" in item.keys(): 1922 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1923 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1924 1925 else: 1926 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1927 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1928 1929 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1930 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1931 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1932 1933 else: 1934 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1935 1936 # requested price when stop-order executed: 1937 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1938 1939 # price for limit-order, set up when stop-order executed: 1940 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1941 1942 # necessary changes in percent to reach target from current price: 1943 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1944 1945 view["stat"]["stopOrders"].append({ 1946 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1947 "figi": item["figi"], # FIGI identification 1948 "ticker": instrument["ticker"], # ticker name by FIGI 1949 "lotsRequested": item["lotsRequested"], # requested lots value 1950 "currentPrice": lastPrice, # current instrument's price for defined action 1951 "targetPrice": target, # requested price for stop-order execution in base currency 1952 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1953 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1954 "percentChanges": changes, # changes in percent to target from current price 1955 "currency": item["currency"], # instrument's currency name 1956 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1957 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1958 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1959 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1960 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1961 }) 1962 1963 # --- calculating data for analytics section: 1964 # portfolio distribution by assets: 1965 view["analytics"]["distrByAssets"] = { 1966 "Ruble": { 1967 "uniques": 1, 1968 "cost": view["stat"]["availableRUB"], 1969 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1970 }, 1971 "Currencies": { 1972 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1973 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1974 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1975 }, 1976 "Shares": { 1977 "uniques": len(view["stat"]["Shares"]), 1978 "cost": view["stat"]["sharesCostRUB"], 1979 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1980 }, 1981 "Bonds": { 1982 "uniques": len(view["stat"]["Bonds"]), 1983 "cost": view["stat"]["bondsCostRUB"], 1984 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Etfs": { 1987 "uniques": len(view["stat"]["Etfs"]), 1988 "cost": view["stat"]["etfsCostRUB"], 1989 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Futures": { 1992 "uniques": len(view["stat"]["Futures"]), 1993 "cost": view["stat"]["futuresCostRUB"], 1994 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 } 1997 1998 # portfolio distribution by companies: 1999 view["analytics"]["distrByCompanies"]["All money cash"] = { 2000 "ticker": "", 2001 "cost": view["stat"]["allCurrenciesCostRUB"], 2002 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2003 } 2004 view["analytics"]["distrByCompanies"].update(byComp) 2005 2006 # portfolio distribution by sectors: 2007 view["analytics"]["distrBySectors"]["All money cash"] = { 2008 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2009 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2010 } 2011 view["analytics"]["distrBySectors"].update(bySect) 2012 2013 # portfolio distribution by currencies: 2014 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2015 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2016 2017 if self.moreDebug: 2018 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2019 2020 view["analytics"]["distrByCurrencies"].update(byCurr) 2021 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2022 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2023 2024 # portfolio distribution by countries: 2025 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2026 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2027 2028 if self.moreDebug: 2029 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2030 2031 view["analytics"]["distrByCountries"].update(byCountry) 2032 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2033 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2034 2035 # --- Prepare text statistics overview in human-readable: 2036 if show: 2037 # Whatever the value `details`, header not changes: 2038 info = [ 2039 "# Client's portfolio\n\n", 2040 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2041 "* **Account ID:** [{}]\n".format(self.accountId), 2042 ] 2043 2044 if details in ["full", "positions", "digest"]: 2045 info.extend([ 2046 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2047 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2048 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2049 view["stat"]["totalChangesRUB"], 2050 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2051 view["stat"]["totalChangesPercentRUB"], 2052 ), 2053 ]) 2054 2055 if details in ["full", "positions"]: 2056 info.extend([ 2057 "## Open positions\n\n", 2058 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2059 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2060 "| Ruble | {:>31} | | | | | |\n".format( 2061 "{:.2f} ({:.2f}) rub".format( 2062 view["stat"]["availableRUB"], 2063 view["stat"]["blockedRUB"], 2064 ) 2065 ) 2066 ]) 2067 2068 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2069 return [ 2070 "| | | | | | | |\n", 2071 "| {:<27} | | | | | {:>19} | |\n".format( 2072 noTradeStr if noTradeStr else typeStr, 2073 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2074 ), 2075 ] 2076 2077 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2078 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2079 "{} [{}]".format(data["ticker"], data["figi"]), 2080 "{:.2f} ({:.2f}) {}".format( 2081 data["volume"], 2082 data["blocked"], 2083 data["currency"], 2084 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2085 data["volume"], 2086 data["blocked"], 2087 ), 2088 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2089 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2090 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2092 "{}{:.2f} {} ({}{:.2f}%)".format( 2093 "+" if data["profit"] > 0 else "", 2094 data["profit"], data["baseCurrencyName"], 2095 "+" if data["percentProfit"] > 0 else "", 2096 data["percentProfit"], 2097 ), 2098 ) 2099 2100 # --- Show currencies section: 2101 if view["stat"]["Currencies"]: 2102 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2103 for item in view["stat"]["Currencies"]: 2104 info.append(_InfoStr(item, showCurrencyName=True)) 2105 2106 else: 2107 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2108 2109 # --- Show shares section: 2110 if view["stat"]["Shares"]: 2111 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2112 2113 for item in view["stat"]["Shares"]: 2114 info.append(_InfoStr(item)) 2115 2116 else: 2117 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2118 2119 # --- Show bonds section: 2120 if view["stat"]["Bonds"]: 2121 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2122 2123 for item in view["stat"]["Bonds"]: 2124 info.append(_InfoStr(item)) 2125 2126 else: 2127 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2128 2129 # --- Show etfs section: 2130 if view["stat"]["Etfs"]: 2131 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2132 2133 for item in view["stat"]["Etfs"]: 2134 info.append(_InfoStr(item)) 2135 2136 else: 2137 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2138 2139 # --- Show futures section: 2140 if view["stat"]["Futures"]: 2141 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2142 2143 for item in view["stat"]["Futures"]: 2144 info.append(_InfoStr(item)) 2145 2146 else: 2147 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2148 2149 if details in ["full", "orders"]: 2150 # --- Show pending limit orders section: 2151 if view["stat"]["orders"]: 2152 info.extend([ 2153 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2154 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2155 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2156 ]) 2157 2158 for item in view["stat"]["orders"]: 2159 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2160 "{} [{}]".format(item["ticker"], item["figi"]), 2161 item["orderID"], 2162 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2163 "{} {} ({}{:.2f}%)".format( 2164 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2165 item["baseCurrencyName"], 2166 "+" if item["percentChanges"] > 0 else "", 2167 float(item["percentChanges"]), 2168 ), 2169 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2170 item["action"], 2171 item["type"], 2172 item["date"], 2173 )) 2174 2175 else: 2176 info.append("\n## Total pending limit-orders: 0\n") 2177 2178 # --- Show stop orders section: 2179 if view["stat"]["stopOrders"]: 2180 info.extend([ 2181 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2182 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2183 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2184 ]) 2185 2186 for item in view["stat"]["stopOrders"]: 2187 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2188 "{} [{}]".format(item["ticker"], item["figi"]), 2189 item["orderID"], 2190 item["lotsRequested"], 2191 "{} {} ({}{:.2f}%)".format( 2192 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2193 item["baseCurrencyName"], 2194 "+" if item["percentChanges"] > 0 else "", 2195 float(item["percentChanges"]), 2196 ), 2197 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2198 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2199 item["action"], 2200 item["type"], 2201 item["expType"], 2202 item["createDate"], 2203 item["expDate"], 2204 )) 2205 2206 else: 2207 info.append("\n## Total stop-orders: 0\n") 2208 2209 if details in ["full", "analytics"]: 2210 # -- Show analytics section: 2211 if view["stat"]["portfolioCostRUB"] > 0: 2212 info.extend([ 2213 "\n# Analytics\n" 2214 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2215 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2216 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2217 view["stat"]["totalChangesRUB"], 2218 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2219 view["stat"]["totalChangesPercentRUB"], 2220 ), 2221 "\n## Portfolio distribution by assets\n" 2222 "\n| Type | Uniques | Percent | Current cost |\n", 2223 "|------------------------------------|---------|---------|--------------------|\n", 2224 ]) 2225 2226 for key in view["analytics"]["distrByAssets"].keys(): 2227 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2228 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2229 key, 2230 view["analytics"]["distrByAssets"][key]["uniques"], 2231 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2232 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2233 )) 2234 2235 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2236 2237 info.extend([ 2238 "\n## Portfolio distribution by companies\n" 2239 "\n| Company | Percent | Current cost |\n", 2240 aSepLine, 2241 ]) 2242 2243 for company in view["analytics"]["distrByCompanies"].keys(): 2244 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2245 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2246 "{}{}".format( 2247 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2248 company, 2249 ), 2250 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2251 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2252 )) 2253 2254 info.extend([ 2255 "\n## Portfolio distribution by sectors\n" 2256 "\n| Sector | Percent | Current cost |\n", 2257 aSepLine, 2258 ]) 2259 2260 for sector in view["analytics"]["distrBySectors"].keys(): 2261 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2262 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2263 sector, 2264 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2265 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2266 )) 2267 2268 info.extend([ 2269 "\n## Portfolio distribution by currencies\n" 2270 "\n| Instruments currencies | Percent | Current cost |\n", 2271 aSepLine, 2272 ]) 2273 2274 for curr in view["analytics"]["distrByCurrencies"].keys(): 2275 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2276 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2277 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2278 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2279 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2280 )) 2281 2282 info.extend([ 2283 "\n## Portfolio distribution by countries\n" 2284 "\n| Assets by country | Percent | Current cost |\n", 2285 aSepLine, 2286 ]) 2287 2288 for country in view["analytics"]["distrByCountries"].keys(): 2289 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2290 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2291 country, 2292 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2293 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2294 )) 2295 2296 if details in ["full", "calendar"]: 2297 # -- Show bonds payment calendar section: 2298 if view["stat"]["Bonds"]: 2299 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2300 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2301 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2302 2303 else: 2304 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2305 2306 infoText = "".join(info) 2307 2308 uLogger.info(infoText) 2309 2310 if details == "full" and self.overviewFile: 2311 filename = self.overviewFile 2312 2313 elif details == "digest" and self.overviewDigestFile: 2314 filename = self.overviewDigestFile 2315 2316 elif details == "positions" and self.overviewPositionsFile: 2317 filename = self.overviewPositionsFile 2318 2319 elif details == "orders" and self.overviewOrdersFile: 2320 filename = self.overviewOrdersFile 2321 2322 elif details == "analytics" and self.overviewAnalyticsFile: 2323 filename = self.overviewAnalyticsFile 2324 2325 elif details == "calendar" and self.overviewBondsCalendarFile: 2326 filename = self.overviewBondsCalendarFile 2327 2328 else: 2329 filename = "" 2330 2331 if filename: 2332 with open(filename, "w", encoding="UTF-8") as fH: 2333 fH.write(infoText) 2334 2335 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2336 2337 return view 2338 2339 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2340 """ 2341 Returns history operations between two given dates for current `accountId`. 2342 If `reportFile` string is not empty then also save human-readable report. 2343 Shows some statistical data of closed positions. 2344 2345 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2346 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2347 :param show: if `True` then also prints all records to the console. 2348 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2349 :return: original list of dictionaries with history of deals records from API ("operations" key): 2350 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2351 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2352 """ 2353 if self.accountId is None or not self.accountId: 2354 uLogger.error("Variable `accountId` must be defined for using this method!") 2355 raise Exception("Account ID required") 2356 2357 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2358 2359 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2360 2361 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2362 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2363 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2364 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2365 customStat = {} # custom statistics in additional to responseJSON 2366 2367 # --- output report in human-readable format: 2368 if show or self.reportFile: 2369 splitLine1 = "| | | | | |\n" # Summary section 2370 splitLine2 = "| | | | | | | | |\n" # Operations section 2371 nextDay = "" 2372 2373 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2374 2375 if len(ops) > 0: 2376 customStat = { 2377 "opsCount": 0, # total operations count 2378 "buyCount": 0, # buy operations 2379 "sellCount": 0, # sell operations 2380 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2381 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2382 "payIn": {"rub": 0.}, # Deposit brokerage account 2383 "payOut": {"rub": 0.}, # Withdrawals 2384 "divs": {"rub": 0.}, # Dividends income 2385 "coupons": {"rub": 0.}, # Coupon's income 2386 "brokerCom": {"rub": 0.}, # Service commissions 2387 "serviceCom": {"rub": 0.}, # Service commissions 2388 "marginCom": {"rub": 0.}, # Margin commissions 2389 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2390 } 2391 2392 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2393 for item in ops: 2394 if item["state"] == "OPERATION_STATE_EXECUTED": 2395 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2396 2397 # count buy operations: 2398 if "_BUY" in item["operationType"]: 2399 customStat["buyCount"] += 1 2400 2401 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2402 customStat["buyTotal"][item["payment"]["currency"]] += payment 2403 2404 else: 2405 customStat["buyTotal"][item["payment"]["currency"]] = payment 2406 2407 # count sell operations: 2408 elif "_SELL" in item["operationType"]: 2409 customStat["sellCount"] += 1 2410 2411 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2412 customStat["sellTotal"][item["payment"]["currency"]] += payment 2413 2414 else: 2415 customStat["sellTotal"][item["payment"]["currency"]] = payment 2416 2417 # count incoming operations: 2418 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2419 if item["payment"]["currency"] in customStat["payIn"].keys(): 2420 customStat["payIn"][item["payment"]["currency"]] += payment 2421 2422 else: 2423 customStat["payIn"][item["payment"]["currency"]] = payment 2424 2425 # count withdrawals operations: 2426 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2427 if item["payment"]["currency"] in customStat["payOut"].keys(): 2428 customStat["payOut"][item["payment"]["currency"]] += payment 2429 2430 else: 2431 customStat["payOut"][item["payment"]["currency"]] = payment 2432 2433 # count dividends income: 2434 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2435 if item["payment"]["currency"] in customStat["divs"].keys(): 2436 customStat["divs"][item["payment"]["currency"]] += payment 2437 2438 else: 2439 customStat["divs"][item["payment"]["currency"]] = payment 2440 2441 # count coupon's income: 2442 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2443 if item["payment"]["currency"] in customStat["coupons"].keys(): 2444 customStat["coupons"][item["payment"]["currency"]] += payment 2445 2446 else: 2447 customStat["coupons"][item["payment"]["currency"]] = payment 2448 2449 # count broker commissions: 2450 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2451 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2452 customStat["brokerCom"][item["payment"]["currency"]] += payment 2453 2454 else: 2455 customStat["brokerCom"][item["payment"]["currency"]] = payment 2456 2457 # count service commissions: 2458 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2459 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2460 customStat["serviceCom"][item["payment"]["currency"]] += payment 2461 2462 else: 2463 customStat["serviceCom"][item["payment"]["currency"]] = payment 2464 2465 # count margin commissions: 2466 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2467 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2468 customStat["marginCom"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["marginCom"][item["payment"]["currency"]] = payment 2472 2473 # count withholding taxes: 2474 elif "_TAX" in item["operationType"]: 2475 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2476 customStat["allTaxes"][item["payment"]["currency"]] += payment 2477 2478 else: 2479 customStat["allTaxes"][item["payment"]["currency"]] = payment 2480 2481 else: 2482 continue 2483 2484 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2485 2486 # --- view "Actions" lines: 2487 info.extend([ 2488 "| Report sections | | | | |\n", 2489 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2490 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2491 "| | Buy: {:<22} | {:<28} | | |\n".format( 2492 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2493 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2494 ), 2495 "| | Sell: {:<21} | {:<28} | | |\n".format( 2496 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2497 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2498 ), 2499 ]) 2500 2501 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2502 for key in opsKeys: 2503 if key == "rub": 2504 continue 2505 2506 info.extend([ 2507 "| | | {:<28} | | |\n".format( 2508 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2509 ), 2510 "| | | {:<28} | | |\n".format( 2511 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2512 ), 2513 ]) 2514 2515 info.append(splitLine1) 2516 2517 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2518 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2519 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2520 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2521 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2523 ) 2524 2525 # --- view "Payments" lines: 2526 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2527 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2528 2529 for key in paymentsKeys: 2530 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2531 2532 info.append(splitLine1) 2533 2534 # --- view "Commissions and taxes" lines: 2535 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2536 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2537 2538 for key in comKeys: 2539 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2540 2541 info.append(splitLine1) 2542 2543 info.extend([ 2544 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2545 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2546 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2547 ]) 2548 2549 else: 2550 info.append("Broker returned no operations during this period\n") 2551 2552 # --- view "Operations" section: 2553 for item in ops: 2554 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2555 continue 2556 2557 else: 2558 self._figi = item["figi"] if item["figi"] else "" 2559 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2560 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2561 2562 # group of deals during one day: 2563 if nextDay and item["date"].split("T")[0] != nextDay: 2564 info.append(splitLine2) 2565 nextDay = "" 2566 2567 else: 2568 nextDay = item["date"].split("T")[0] # saving current day for splitting 2569 2570 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2571 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2572 self._figi if self._figi else "—", 2573 instrument["ticker"] if instrument else "—", 2574 instrument["type"] if instrument else "—", 2575 item["quantity"] if int(item["quantity"]) > 0 else "—", 2576 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2577 TKS_OPERATION_STATES[item["state"]], 2578 TKS_OPERATION_TYPES[item["operationType"]], 2579 )) 2580 2581 infoText = "".join(info) 2582 2583 if show: 2584 if self.moreDebug: 2585 uLogger.debug("Records about history of a client's operations successfully received") 2586 2587 uLogger.info(infoText) 2588 2589 if self.reportFile: 2590 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2591 fH.write(infoText) 2592 2593 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2594 2595 return ops, customStat 2596 2597 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2598 """ 2599 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2600 2601 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2602 Warning! Broker server used ISO UTC time by default. 2603 2604 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2605 Also, `historyFile` used to update history with `onlyMissing` parameter. 2606 2607 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2608 2609 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2610 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2611 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2612 `"hour"`, `"day"`. Default: `"hour"`. 2613 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2614 False by default. Warning! History appends only from last candle to current time 2615 with always update last candle! 2616 :param csvSep: separator if csv-file is used, `,` by default. 2617 :param show: if `True` then also prints Pandas DataFrame to the console. 2618 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2619 `["date", "time", "open", "high", "low", "close", "volume"]`. 2620 """ 2621 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2622 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2623 history = None # empty pandas object for history 2624 2625 if interval not in TKS_CANDLE_INTERVALS.keys(): 2626 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2627 raise Exception("Incorrect value") 2628 2629 if not (self._ticker or self._figi): 2630 uLogger.error("Ticker or FIGI must be defined!") 2631 raise Exception("Ticker or FIGI required") 2632 2633 if self._ticker and not self._figi: 2634 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2635 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2636 2637 if self._figi and not self._ticker: 2638 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2639 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2640 2641 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2642 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2643 if interval.lower() != "day": 2644 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2645 2646 delta = dtEnd - dtStart # current UTC time minus last time in file 2647 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2648 2649 # calculate history length in candles: 2650 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2651 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2652 length += 1 # to avoid fraction time 2653 2654 # calculate data blocks count: 2655 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2656 2657 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2658 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2659 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2660 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2661 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2662 2663 tempOld = None # pandas object for old history, if --only-missing key present 2664 lastTime = None # datetime object of last old candle in file 2665 2666 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2667 uLogger.debug("--only-missing key present, add only last missing candles...") 2668 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2669 2670 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2671 2672 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2673 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2674 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2675 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2676 2677 # get last datetime object from last string in file or minus 1 delta if file is empty: 2678 if len(tempOld) > 0: 2679 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2680 2681 else: 2682 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2683 2684 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2685 2686 responseJSONs = [] # raw history blocks of data 2687 2688 blockEnd = dtEnd 2689 for item in range(blocks): 2690 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2691 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2692 2693 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2694 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2695 )) 2696 2697 if blockStart == blockEnd: 2698 uLogger.debug("Skipped this zero-length block...") 2699 2700 else: 2701 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2702 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2703 self.body = str({ 2704 "figi": self._figi, 2705 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2706 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2707 "interval": TKS_CANDLE_INTERVALS[interval][0] 2708 }) 2709 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2710 2711 if "code" in responseJSON.keys(): 2712 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2713 2714 else: 2715 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2716 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2717 2718 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2719 2720 blockEnd = blockStart 2721 2722 printCount = len(responseJSONs) # candles to show in console 2723 if responseJSONs: 2724 tempHistory = pd.DataFrame( 2725 data={ 2726 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2727 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2728 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2729 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2730 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2731 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2732 "volume": [int(item["volume"]) for item in responseJSONs], 2733 }, 2734 index=range(len(responseJSONs)), 2735 columns=["date", "time", "open", "high", "low", "close", "volume"], 2736 ) 2737 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2738 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2739 2740 # append only newest candles to old history if --only-missing key present: 2741 if onlyMissing and tempOld is not None and lastTime is not None: 2742 index = 0 # find start index in tempHistory data: 2743 2744 for i, item in tempHistory.iterrows(): 2745 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2746 2747 if curTime == lastTime: 2748 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2749 index = i 2750 printCount = index + 1 2751 break 2752 2753 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2754 2755 else: 2756 history = tempHistory # if no `--only-missing` key then load full data from server 2757 2758 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2759 2760 if history is not None and not history.empty: 2761 if show: 2762 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2763 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2764 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2765 )) 2766 2767 else: 2768 uLogger.warning("Received an empty candles history!") 2769 2770 if self.historyFile is not None: 2771 if history is not None and not history.empty: 2772 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2773 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2774 2775 else: 2776 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2777 2778 else: 2779 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2780 2781 return history 2782 2783 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2784 """ 2785 Load candles history from csv-file and return Pandas DataFrame object. 2786 2787 See also: `History()` and `ShowHistoryChart()` methods. 2788 2789 :param filePath: path to csv-file to open. 2790 """ 2791 loadedHistory = None # init candles data object 2792 2793 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2794 2795 if os.path.exists(filePath): 2796 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2797 2798 tfStr = self.priceModel.FormattedDelta( 2799 self.priceModel.timeframe, 2800 "{days} days {hours}h {minutes}m {seconds}s", 2801 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2802 self.priceModel.timeframe, 2803 "{hours}h {minutes}m {seconds}s", 2804 ) 2805 2806 if loadedHistory is not None and not loadedHistory.empty: 2807 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2808 len(loadedHistory), 2809 tfStr, 2810 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2811 ) 2812 2813 else: 2814 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2815 2816 else: 2817 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2818 2819 return loadedHistory 2820 2821 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2822 """ 2823 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2824 2825 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2826 Default: `index.html` (both for interact and non-interact candlesticks chart). 2827 2828 See also: `History()` and `LoadHistory()` methods. 2829 2830 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2831 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2832 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2833 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2834 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2835 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2836 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2837 """ 2838 if isinstance(candles, str): 2839 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2840 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2841 2842 elif isinstance(candles, pd.DataFrame): 2843 self.priceModel.prices = candles # set candles chain from variable 2844 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2845 2846 if "datetime" not in candles.columns: 2847 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2848 2849 else: 2850 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2851 raise Exception("Incorrect value") 2852 2853 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2854 2855 if interact: 2856 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2857 2858 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2859 2860 else: 2861 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2862 2863 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2864 2865 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2866 2867 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2868 """ 2869 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2870 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2871 2872 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2873 2874 :param operation: string "Buy" or "Sell". 2875 :param lots: volume, integer count of lots >= 1. 2876 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2877 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2878 :param expDate: string "Undefined" by default or local date in future, 2879 it is a string with format `%Y-%m-%d %H:%M:%S`. 2880 :return: JSON with response from broker server. 2881 """ 2882 if self.accountId is None or not self.accountId: 2883 uLogger.error("Variable `accountId` must be defined for using this method!") 2884 raise Exception("Account ID required") 2885 2886 if operation is None or not operation or operation not in ("Buy", "Sell"): 2887 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2888 raise Exception("Incorrect value") 2889 2890 if lots is None or lots < 1: 2891 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2892 lots = 1 2893 2894 if tp is None or tp < 0: 2895 tp = 0 2896 2897 if sl is None or sl < 0: 2898 sl = 0 2899 2900 if expDate is None or not expDate: 2901 expDate = "Undefined" 2902 2903 if not (self._ticker or self._figi): 2904 uLogger.error("Ticker or FIGI must be defined!") 2905 raise Exception("Ticker or FIGI required") 2906 2907 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2908 self._ticker = instrument["ticker"] 2909 self._figi = instrument["figi"] 2910 2911 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2912 2913 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2914 self.body = str({ 2915 "figi": self._figi, 2916 "quantity": str(lots), 2917 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2918 "accountId": str(self.accountId), 2919 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2920 }) 2921 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2922 2923 if "orderId" in response.keys(): 2924 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2925 operation, response["orderId"], 2926 self._ticker, self._figi, lots, 2927 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2928 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2929 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2930 )) 2931 2932 if tp > 0: 2933 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2934 2935 if sl > 0: 2936 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2937 2938 else: 2939 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2940 2941 return response 2942 2943 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2944 """ 2945 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2946 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2947 2948 See also: `Order()` and `Trade()` docstrings. 2949 2950 :param lots: volume, integer count of lots >= 1. 2951 :param tp: float > 0, take profit price of stop-order. 2952 :param sl: float > 0, stop loss price of stop-order. 2953 :param expDate: it's a local date in future. 2954 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2955 :return: JSON with response from broker server. 2956 """ 2957 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2958 2959 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2960 """ 2961 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2962 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2963 2964 See also: `Order()` and `Trade()` docstrings. 2965 2966 :param lots: volume, integer count of lots >= 1. 2967 :param tp: float > 0, take profit price of stop-order. 2968 :param sl: float > 0, stop loss price of stop-order. 2969 :param expDate: it's a local date in the future. 2970 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2971 :return: JSON with response from broker server. 2972 """ 2973 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2974 2975 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2976 """ 2977 Close position of given instruments. 2978 2979 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2980 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2981 This avoids unnecessary downloading data from the server. 2982 """ 2983 if instruments is None or not instruments: 2984 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2985 raise Exception("Ticker or FIGI required") 2986 2987 if isinstance(instruments, str): 2988 instruments = [instruments] 2989 2990 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2991 if uniqueInstruments: 2992 if portfolio is None or not portfolio: 2993 portfolio = self.Overview(show=False) 2994 2995 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2996 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2997 2998 for self._figi in uniqueInstruments: 2999 if self._figi not in allOpened: 3000 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3001 continue 3002 3003 # search open trade info about instrument by ticker: 3004 instrument = {} 3005 for iType in TKS_INSTRUMENTS: 3006 if instrument: 3007 break 3008 3009 for item in portfolio["stat"][iType]: 3010 if item["figi"] == self._figi: 3011 instrument = item 3012 break 3013 3014 if instrument: 3015 self._ticker = instrument["ticker"] 3016 self._figi = instrument["figi"] 3017 3018 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3019 self._ticker, 3020 self._figi, 3021 int(instrument["volume"]), 3022 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3023 )) 3024 3025 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3026 3027 if tradeLots > 0: 3028 if instrument["blocked"] > 0: 3029 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3030 instrument["blocked"], 3031 self._ticker, 3032 tradeLots, 3033 )) 3034 3035 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3036 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3037 3038 else: 3039 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3040 3041 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3042 """ 3043 Close all positions of given instruments with defined type. 3044 3045 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3046 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3047 This avoids unnecessary downloading data from the server. 3048 """ 3049 if iType not in TKS_INSTRUMENTS: 3050 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3051 3052 else: 3053 if portfolio is None or not portfolio: 3054 portfolio = self.Overview(show=False) 3055 3056 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3057 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3058 3059 if tickers and portfolio: 3060 self.CloseTrades(tickers, portfolio) 3061 3062 else: 3063 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3064 3065 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3066 """ 3067 Universal method to create market or limit orders with all available parameters for current `accountId`. 3068 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3069 3070 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3071 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3072 3073 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3074 then broker immediately open market order as you can do simple --buy or --sell operations! 3075 3076 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3077 When current price will go up or down to target price value then broker opens a limit order. 3078 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3079 3080 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3081 3082 :param operation: string "Buy" or "Sell". 3083 :param orderType: string "Limit" or "Stop". 3084 :param lots: volume, integer count of lots >= 1. 3085 :param targetPrice: target price > 0. This is open trade price for limit order. 3086 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3087 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3088 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3089 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3090 Stop loss order always executed by market price. 3091 :param expDate: string "Undefined" by default or local date in future. 3092 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3093 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3094 A limit order has no expiration date, it lasts until the end of the trading day. 3095 :return: JSON with response from broker server. 3096 """ 3097 if self.accountId is None or not self.accountId: 3098 uLogger.error("Variable `accountId` must be defined for using this method!") 3099 raise Exception("Account ID required") 3100 3101 if operation is None or not operation or operation not in ("Buy", "Sell"): 3102 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3103 raise Exception("Incorrect value") 3104 3105 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3106 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3107 raise Exception("Incorrect value") 3108 3109 if lots is None or lots < 1: 3110 uLogger.error("You must define trade volume > 0: integer count of lots!") 3111 raise Exception("Incorrect value") 3112 3113 if targetPrice is None or targetPrice <= 0: 3114 uLogger.error("Target price for limit-order must be greater than 0!") 3115 raise Exception("Incorrect value") 3116 3117 if limitPrice is None or limitPrice <= 0: 3118 limitPrice = targetPrice 3119 3120 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3121 stopType = "Limit" 3122 3123 if expDate is None or not expDate: 3124 expDate = "Undefined" 3125 3126 if not (self._ticker or self._figi): 3127 uLogger.error("Tocker or FIGI must be defined!") 3128 raise Exception("Ticker or FIGI required") 3129 3130 response = {} 3131 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3132 self._ticker = instrument["ticker"] 3133 self._figi = instrument["figi"] 3134 3135 if orderType == "Limit": 3136 uLogger.debug( 3137 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3138 self._ticker, self._figi, 3139 operation, lots, targetPrice, instrument["currency"], 3140 )) 3141 3142 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3143 self.body = str({ 3144 "figi": self._figi, 3145 "quantity": str(lots), 3146 "price": FloatToNano(targetPrice), 3147 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3148 "accountId": str(self.accountId), 3149 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3150 }) 3151 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3152 3153 if "orderId" in response.keys(): 3154 uLogger.info( 3155 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3156 response["orderId"], 3157 self._ticker, self._figi, 3158 operation, lots, targetPrice, instrument["currency"], 3159 )) 3160 3161 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3162 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3163 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3164 targetPrice, instrument["currency"], 3165 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3166 )) 3167 3168 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3169 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3170 targetPrice, instrument["currency"], 3171 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3172 )) 3173 3174 else: 3175 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3176 3177 if orderType == "Stop": 3178 uLogger.debug( 3179 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3180 self._ticker, self._figi, 3181 operation, lots, 3182 targetPrice, instrument["currency"], 3183 limitPrice, instrument["currency"], 3184 stopType, expDate, 3185 )) 3186 3187 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3188 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3189 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3190 3191 body = { 3192 "figi": self._figi, 3193 "quantity": str(lots), 3194 "price": FloatToNano(limitPrice), 3195 "stopPrice": FloatToNano(targetPrice), 3196 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3197 "accountId": str(self.accountId), 3198 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3199 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3200 } 3201 3202 if expDateUTC: 3203 body["expireDate"] = expDateUTC 3204 3205 self.body = str(body) 3206 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3207 3208 if "stopOrderId" in response.keys(): 3209 uLogger.info( 3210 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3211 response["stopOrderId"], 3212 self._ticker, self._figi, 3213 operation, lots, 3214 targetPrice, instrument["currency"], 3215 limitPrice, instrument["currency"], 3216 TKS_STOP_ORDER_TYPES[stopOrderType], 3217 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3218 )) 3219 3220 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3221 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3222 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3223 targetPrice, instrument["currency"], 3224 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3225 )) 3226 3227 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3228 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3229 targetPrice, instrument["currency"], 3230 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3231 )) 3232 3233 else: 3234 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3235 3236 return response 3237 3238 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3239 """ 3240 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3241 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3242 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3243 See also: `Order()` docstring. 3244 3245 :param lots: volume, integer count of lots >= 1. 3246 :param targetPrice: target price > 0. This is open trade price for limit order. 3247 :return: JSON with response from broker server. 3248 """ 3249 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3250 3251 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3252 """ 3253 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3254 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3255 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3256 target price value then broker opens a limit order. See also: `Order()` docstring. 3257 3258 :param lots: volume, integer count of lots >= 1. 3259 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3260 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3261 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3262 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3263 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3264 :param expDate: string "Undefined" by default or local date in future. 3265 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3266 This date is converting to UTC format for server. 3267 :return: JSON with response from broker server. 3268 """ 3269 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3270 3271 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3272 """ 3273 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3274 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3275 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3276 See also: `Order()` docstring. 3277 3278 :param lots: volume, integer count of lots >= 1. 3279 :param targetPrice: target price > 0. This is open trade price for limit order. 3280 :return: JSON with response from broker server. 3281 """ 3282 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3283 3284 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3285 """ 3286 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3287 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3288 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3289 target price value then broker opens a limit order. See also: `Order()` docstring. 3290 3291 :param lots: volume, integer count of lots >= 1. 3292 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3293 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3294 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3295 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3296 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3297 :param expDate: string "Undefined" by default or local date in future. 3298 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3299 This date is converting to UTC format for server. 3300 :return: JSON with response from broker server. 3301 """ 3302 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3303 3304 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3305 """ 3306 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3307 3308 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3309 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3310 This avoids unnecessary downloading data from the server. 3311 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3312 """ 3313 if self.accountId is None or not self.accountId: 3314 uLogger.error("Variable `accountId` must be defined for using this method!") 3315 raise Exception("Account ID required") 3316 3317 if orderIDs: 3318 if allOrdersIDs is None: 3319 rawOrders = self.RequestPendingOrders() 3320 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3321 3322 if allStopOrdersIDs is None: 3323 rawStopOrders = self.RequestStopOrders() 3324 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3325 3326 for orderID in orderIDs: 3327 idInPendingOrders = orderID in allOrdersIDs 3328 idInStopOrders = orderID in allStopOrdersIDs 3329 3330 if not (idInPendingOrders or idInStopOrders): 3331 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3332 continue 3333 3334 else: 3335 if idInPendingOrders: 3336 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3337 3338 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3339 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3340 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3341 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3342 3343 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3344 if self.moreDebug: 3345 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3346 3347 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3348 3349 else: 3350 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3351 3352 elif idInStopOrders: 3353 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3354 3355 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3356 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3357 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3358 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3359 3360 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3361 if self.moreDebug: 3362 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3363 3364 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3365 3366 else: 3367 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3368 3369 else: 3370 continue 3371 3372 def CloseAllOrders(self) -> None: 3373 """ 3374 Gets a list of open pending and stop orders and cancel it all. 3375 """ 3376 rawOrders = self.RequestPendingOrders() 3377 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3378 lenOrders = len(allOrdersIDs) 3379 3380 rawStopOrders = self.RequestStopOrders() 3381 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3382 lenSOrders = len(allStopOrdersIDs) 3383 3384 if lenOrders > 0 or lenSOrders > 0: 3385 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3386 3387 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3388 3389 else: 3390 uLogger.info("Orders not found, nothing to cancel.") 3391 3392 def CloseAll(self, *args) -> None: 3393 """ 3394 Close all available (not blocked) opened trades and orders. 3395 3396 Also, you can select one or more keywords case-insensitive: 3397 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3398 3399 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3400 """ 3401 overview = self.Overview(show=False) # get all open trades info 3402 3403 if len(args) == 0: 3404 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3405 self.CloseAllOrders() # close all pending and stop orders 3406 3407 for iType in TKS_INSTRUMENTS: 3408 if iType != "Currencies": 3409 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3410 3411 else: 3412 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3413 lowerArgs = [x.lower() for x in args] 3414 3415 if "orders" in lowerArgs: 3416 self.CloseAllOrders() # close all pending and stop orders 3417 3418 for iType in TKS_INSTRUMENTS: 3419 if iType.lower() in lowerArgs and iType != "Currencies": 3420 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3421 3422 def CloseAllByTicker(self, instrument: str) -> None: 3423 """ 3424 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3425 3426 This method searches opened trade and orders of instrument throw all portfolio and then use 3427 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3428 3429 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3430 3431 :param instrument: string with ticker. 3432 """ 3433 if instrument is None or not instrument: 3434 uLogger.error("Ticker name must be defined for using this method!") 3435 raise Exception("Ticker required") 3436 3437 overview = self.Overview(show=False) # get user portfolio with all open trades info 3438 3439 self._ticker = instrument # try to set instrument as ticker 3440 self._figi = "" 3441 3442 if self.IsInPortfolio(portfolio=overview): 3443 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3444 self.CloseTrades(instruments=[instrument], portfolio=overview) 3445 3446 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3447 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3448 3449 if limitAll and self.IsInLimitOrders(portfolio=overview): 3450 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3451 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3452 3453 if stopAll and self.IsInStopOrders(portfolio=overview): 3454 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3455 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3456 3457 def CloseAllByFIGI(self, instrument: str) -> None: 3458 """ 3459 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3460 3461 This method searches opened trade and orders of instrument throw all portfolio and then use 3462 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3463 3464 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3465 3466 :param instrument: string with FIGI id. 3467 """ 3468 if instrument is None or not instrument: 3469 uLogger.error("FIGI id must be defined for using this method!") 3470 raise Exception("FIGI required") 3471 3472 overview = self.Overview(show=False) # get user portfolio with all open trades info 3473 3474 self._ticker = "" 3475 self._figi = instrument # try to set instrument as FIGI id 3476 3477 if self.IsInPortfolio(portfolio=overview): 3478 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3479 self.CloseTrades(instruments=[instrument], portfolio=overview) 3480 3481 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3482 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3483 3484 if limitAll and self.IsInLimitOrders(portfolio=overview): 3485 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3486 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3487 3488 if stopAll and self.IsInStopOrders(portfolio=overview): 3489 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3490 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3491 3492 @staticmethod 3493 def ParseOrderParameters(operation, **inputParameters): 3494 """ 3495 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3496 3497 :param operation: string "Buy" or "Sell". 3498 :param inputParameters: this is dict of strings that looks like this 3499 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3500 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3501 "prices" key: one or more prices to open limit-orders 3502 Counts of values in lots and prices lists must be equals! 3503 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3504 """ 3505 # TODO: update order grid work with api v2 3506 pass 3507 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3508 # 3509 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3510 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3511 # raise Exception("Incorrect value") 3512 # 3513 # if "l" in inputParameters.keys(): 3514 # inputParameters["lots"] = inputParameters.pop("l") 3515 # 3516 # if "p" in inputParameters.keys(): 3517 # inputParameters["prices"] = inputParameters.pop("p") 3518 # 3519 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3520 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3521 # raise Exception("Incorrect value") 3522 # 3523 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3524 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3525 # 3526 # if len(lots) != len(prices): 3527 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3528 # raise Exception("Incorrect value") 3529 # 3530 # uLogger.debug("Extracted parameters for orders:") 3531 # uLogger.debug("lots = {}".format(lots)) 3532 # uLogger.debug("prices = {}".format(prices)) 3533 # 3534 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3535 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3536 # uLogger.debug("Order parameters: {}".format(result)) 3537 # 3538 # return result 3539 3540 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3541 """ 3542 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3543 3544 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3545 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3546 """ 3547 result = False 3548 msg = "Instrument not defined!" 3549 3550 if portfolio is None or not portfolio: 3551 portfolio = self.Overview(show=False) 3552 3553 if self._ticker: 3554 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3555 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3556 3557 for iType in TKS_INSTRUMENTS: 3558 for instrument in portfolio["stat"][iType]: 3559 if instrument["ticker"] == self._ticker: 3560 result = True 3561 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3562 break 3563 3564 elif self._figi: 3565 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3566 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3567 3568 for iType in TKS_INSTRUMENTS: 3569 for instrument in portfolio["stat"][iType]: 3570 if instrument["figi"] == self._figi: 3571 result = True 3572 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3573 break 3574 3575 else: 3576 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3577 3578 uLogger.debug(msg) 3579 3580 return result 3581 3582 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3583 """ 3584 Returns instrument from the user's portfolio if it presents there. 3585 Instrument must be defined by `ticker` (highly priority) or `figi`. 3586 3587 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3588 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3589 """ 3590 result = None 3591 msg = "Instrument not defined!" 3592 3593 if portfolio is None or not portfolio: 3594 portfolio = self.Overview(show=False) 3595 3596 if self._ticker: 3597 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3598 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3599 3600 for iType in TKS_INSTRUMENTS: 3601 for instrument in portfolio["stat"][iType]: 3602 if instrument["ticker"] == self._ticker: 3603 result = instrument 3604 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3605 break 3606 3607 elif self._figi: 3608 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3609 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3610 3611 for iType in TKS_INSTRUMENTS: 3612 for instrument in portfolio["stat"][iType]: 3613 if instrument["figi"] == self._figi: 3614 result = instrument 3615 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3616 break 3617 3618 else: 3619 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3620 3621 uLogger.debug(msg) 3622 3623 return result 3624 3625 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3626 """ 3627 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3628 3629 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3630 3631 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3632 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3633 """ 3634 result = False 3635 msg = "Instrument not defined!" 3636 3637 if portfolio is None or not portfolio: 3638 portfolio = self.Overview(show=False) 3639 3640 if self._ticker: 3641 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3642 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3643 3644 for instrument in portfolio["stat"]["orders"]: 3645 if instrument["ticker"] == self._ticker: 3646 result = True 3647 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3648 break 3649 3650 elif self._figi: 3651 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3652 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3653 3654 for instrument in portfolio["stat"]["orders"]: 3655 if instrument["figi"] == self._figi: 3656 result = True 3657 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3658 break 3659 3660 else: 3661 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3662 3663 uLogger.debug(msg) 3664 3665 return result 3666 3667 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3668 """ 3669 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3670 Instrument must be defined by `ticker` (highly priority) or `figi`. 3671 3672 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3673 3674 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3675 :return: list with `orderID`s of limit orders. 3676 """ 3677 result = [] 3678 msg = "Instrument not defined!" 3679 3680 if portfolio is None or not portfolio: 3681 portfolio = self.Overview(show=False) 3682 3683 if self._ticker: 3684 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3685 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3686 3687 for instrument in portfolio["stat"]["orders"]: 3688 if instrument["ticker"] == self._ticker: 3689 result.append(instrument["orderID"]) 3690 3691 if result: 3692 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3693 3694 elif self._figi: 3695 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3696 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3697 3698 for instrument in portfolio["stat"]["orders"]: 3699 if instrument["figi"] == self._figi: 3700 result.append(instrument["orderID"]) 3701 3702 if result: 3703 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3704 3705 else: 3706 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3707 3708 uLogger.debug(msg) 3709 3710 return result 3711 3712 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3713 """ 3714 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3715 3716 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3717 3718 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3719 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3720 """ 3721 result = False 3722 msg = "Instrument not defined!" 3723 3724 if portfolio is None or not portfolio: 3725 portfolio = self.Overview(show=False) 3726 3727 if self._ticker: 3728 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3729 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3730 3731 for instrument in portfolio["stat"]["stopOrders"]: 3732 if instrument["ticker"] == self._ticker: 3733 result = True 3734 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3735 break 3736 3737 elif self._figi: 3738 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3739 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3740 3741 for instrument in portfolio["stat"]["stopOrders"]: 3742 if instrument["figi"] == self._figi: 3743 result = True 3744 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3745 break 3746 3747 else: 3748 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3749 3750 uLogger.debug(msg) 3751 3752 return result 3753 3754 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3755 """ 3756 Returns list with all `orderID`s of opened stop orders for the instrument. 3757 Instrument must be defined by `ticker` (highly priority) or `figi`. 3758 3759 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3760 3761 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3762 :return: list with `orderID`s of stop orders. 3763 """ 3764 result = [] 3765 msg = "Instrument not defined!" 3766 3767 if portfolio is None or not portfolio: 3768 portfolio = self.Overview(show=False) 3769 3770 if self._ticker: 3771 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3772 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3773 3774 for instrument in portfolio["stat"]["stopOrders"]: 3775 if instrument["ticker"] == self._ticker: 3776 result.append(instrument["orderID"]) 3777 3778 if result: 3779 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3780 3781 elif self._figi: 3782 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3783 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3784 3785 for instrument in portfolio["stat"]["stopOrders"]: 3786 if instrument["figi"] == self._figi: 3787 result.append(instrument["orderID"]) 3788 3789 if result: 3790 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3791 3792 else: 3793 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3794 3795 uLogger.debug(msg) 3796 3797 return result 3798 3799 def RequestLimits(self) -> dict: 3800 """ 3801 Method for obtaining the available funds for withdrawal for current `accountId`. 3802 3803 See also: 3804 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3805 - `OverviewLimits()` method 3806 3807 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3808 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3809 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3810 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3811 """ 3812 if self.accountId is None or not self.accountId: 3813 uLogger.error("Variable `accountId` must be defined for using this method!") 3814 raise Exception("Account ID required") 3815 3816 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3817 3818 self.body = str({"accountId": self.accountId}) 3819 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3820 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3821 3822 if self.moreDebug: 3823 uLogger.debug("Records about available funds for withdrawal successfully received") 3824 3825 return rawLimits 3826 3827 def OverviewLimits(self, show: bool = False) -> dict: 3828 """ 3829 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3830 3831 See also: `RequestLimits()`. 3832 3833 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3834 :return: dict with raw parsed data from server and some calculated statistics about it. 3835 """ 3836 if self.accountId is None or not self.accountId: 3837 uLogger.error("Variable `accountId` must be defined for using this method!") 3838 raise Exception("Account ID required") 3839 3840 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3841 3842 view = { 3843 "rawLimits": rawLimits, 3844 "limits": { # parsed data for every currency: 3845 "money": { # this is an array of portfolio currency positions 3846 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3847 }, 3848 "blocked": { # this is an array of blocked currency 3849 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3850 }, 3851 "blockedGuarantee": { # this is locked money under collateral for futures 3852 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3853 }, 3854 }, 3855 } 3856 3857 # --- Prepare text table with limits in human-readable format: 3858 if show: 3859 info = [ 3860 "# Withdrawal limits\n\n", 3861 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3862 "* **Account ID:** [{}]\n".format(self.accountId), 3863 ] 3864 3865 if view["limits"]["money"]: 3866 info.extend([ 3867 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3868 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3869 ]) 3870 3871 else: 3872 info.append("\nNo withdrawal limits\n") 3873 3874 for curr in view["limits"]["money"].keys(): 3875 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3876 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3877 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3878 3879 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3880 "[{}]".format(curr), 3881 "{:.2f}".format(view["limits"]["money"][curr]), 3882 "{:.2f}".format(availableMoney), 3883 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3884 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3885 ) 3886 3887 if curr == "rub": 3888 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3889 3890 else: 3891 info.append(infoStr) 3892 3893 infoText = "".join(info) 3894 3895 uLogger.info(infoText) 3896 3897 if self.withdrawalLimitsFile: 3898 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3899 fH.write(infoText) 3900 3901 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3902 3903 return view 3904 3905 def RequestAccounts(self) -> dict: 3906 """ 3907 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3908 3909 See also: 3910 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3911 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3912 - `OverviewUserInfo()` method 3913 3914 :return: dict with raw data from server that contains accounts info. Example of dict: 3915 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3916 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3917 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3918 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3919 """ 3920 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3921 3922 self.body = str({}) 3923 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3924 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3925 3926 if self.moreDebug: 3927 uLogger.debug("Records about available accounts successfully received") 3928 3929 return rawAccounts 3930 3931 def RequestUserInfo(self) -> dict: 3932 """ 3933 Method for requesting common user's information. 3934 3935 See also: 3936 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3937 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3938 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3939 - `OverviewUserInfo()` method 3940 3941 :return: dict with raw data from server that contains user's information. Example of dict: 3942 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3943 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3944 """ 3945 uLogger.debug("Requesting common user's information. Wait, please...") 3946 3947 self.body = str({}) 3948 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3949 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3950 3951 if self.moreDebug: 3952 uLogger.debug("Records about current user successfully received") 3953 3954 return rawUserInfo 3955 3956 def RequestMarginStatus(self, accountId: str = None) -> dict: 3957 """ 3958 Method for requesting margin calculation for defined account ID. 3959 3960 See also: 3961 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3962 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3963 - `OverviewUserInfo()` method 3964 3965 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3966 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3967 Example of responses: 3968 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3969 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3970 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3971 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3972 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3973 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3974 """ 3975 if accountId is None or not accountId: 3976 if self.accountId is None or not self.accountId: 3977 uLogger.error("Variable `accountId` must be defined for using this method!") 3978 raise Exception("Account ID required") 3979 3980 else: 3981 accountId = self.accountId # use `self.accountId` (main ID) by default 3982 3983 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3984 3985 self.body = str({"accountId": accountId}) 3986 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3987 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3988 3989 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3990 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3991 rawMargin = {} 3992 3993 else: 3994 if self.moreDebug: 3995 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3996 3997 return rawMargin 3998 3999 def RequestTariffLimits(self) -> dict: 4000 """ 4001 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4002 4003 See also: 4004 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4005 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4006 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4007 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4008 - `OverviewUserInfo()` method 4009 4010 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4011 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4012 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4013 """ 4014 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4015 4016 self.body = str({}) 4017 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4018 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4019 4020 if self.moreDebug: 4021 uLogger.debug("Records with limits of current tariff successfully received") 4022 4023 return rawTariffLimits 4024 4025 def RequestBondCoupons(self, iJSON: dict) -> dict: 4026 """ 4027 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4028 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4029 All dates are in UTC timezone. 4030 4031 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4032 Documentation: 4033 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4034 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4035 4036 See also: `ExtendBondsData()`. 4037 4038 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4039 If raw iJSON is not data of bond then server returns an error [400] with message: 4040 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4041 :return: dictionary with bond payment calendar. Response example 4042 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4043 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4044 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4045 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4046 """ 4047 if iJSON["figi"] is None or not iJSON["figi"]: 4048 uLogger.error("FIGI must be defined for using this method!") 4049 raise Exception("FIGI required") 4050 4051 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4052 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4053 4054 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4055 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4056 self._figi, 4057 startDate, 4058 endDate, 4059 )) 4060 4061 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4062 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4063 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4064 4065 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4066 uLogger.warning("Instrument type is not bond!") 4067 4068 else: 4069 if self.moreDebug: 4070 uLogger.debug("Records about bond payment calendar successfully received") 4071 4072 return calendar 4073 4074 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4075 """ 4076 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4077 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4078 coupon yields, current yields and some statistics etc. 4079 4080 WARNING! This is too long operation if a lot of bonds requested from broker server. 4081 4082 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4083 4084 :param instruments: list of strings with tickers or FIGIs. 4085 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4086 for further used by data scientists or stock analytics. 4087 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4088 In XLSX-file and Pandas DataFrame fields mean: 4089 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4090 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4091 """ 4092 if instruments is None or not instruments: 4093 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4094 raise Exception("Ticker or FIGI required") 4095 4096 if isinstance(instruments, str): 4097 instruments = [instruments] 4098 4099 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4100 4101 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4102 4103 iCount = len(uniqueInstruments) 4104 tooLong = iCount >= 20 4105 if tooLong: 4106 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4107 4108 bonds = None 4109 for i, self._figi in enumerate(uniqueInstruments): 4110 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4111 4112 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4113 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4114 rawBond = self.SearchByFIGI(requestPrice=True) 4115 4116 # Widen raw data with UTC current time (iData["actualDateTime"]): 4117 actualDate = datetime.now(tzutc()) 4118 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4119 4120 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4121 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4122 4123 # Replace some values with human-readable: 4124 iData["nominalCurrency"] = iData["nominal"]["currency"] 4125 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4126 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4127 iData["aciCurrency"] = iData["aciValue"]["currency"] 4128 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4129 iData["issueSize"] = int(iData["issueSize"]) 4130 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4131 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4132 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4133 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4134 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4135 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4136 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4137 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4138 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4139 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4140 4141 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4142 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4143 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4144 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4145 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4146 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4147 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4148 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4149 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4150 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4151 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4152 4153 # Widen raw data with calendar data from `rawCalendar` values: 4154 calendarData = [] 4155 if "events" in iData["rawCalendar"].keys(): 4156 for item in iData["rawCalendar"]["events"]: 4157 calendarData.append({ 4158 "couponDate": item["couponDate"], 4159 "couponNumber": int(item["couponNumber"]), 4160 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4161 "payCurrency": item["payOneBond"]["currency"], 4162 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4163 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4164 "couponStartDate": item["couponStartDate"], 4165 "couponEndDate": item["couponEndDate"], 4166 "couponPeriod": item["couponPeriod"], 4167 }) 4168 4169 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4170 if "maturityDate" not in iData.keys(): 4171 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4172 4173 # Widen raw data with Coupon Rate. 4174 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4175 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4176 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4177 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4178 4179 # Widen raw data with Yield to Maturity (YTM) on current date. 4180 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4181 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4182 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4183 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4184 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4185 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4186 4187 iData["calendar"] = calendarData # adds calendar at the end 4188 4189 # Remove not used data: 4190 iData.pop("uid") 4191 iData.pop("positionUid") 4192 iData.pop("currentPrice") 4193 iData.pop("rawCalendar") 4194 4195 colNames = list(iData.keys()) 4196 if bonds is None: 4197 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4198 4199 else: 4200 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4201 4202 else: 4203 uLogger.warning("Instrument is not a bond!") 4204 4205 processed = round(100 * (i + 1) / iCount, 1) 4206 if tooLong and processed % 5 == 0: 4207 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4208 4209 else: 4210 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4211 4212 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4213 4214 # Saving bonds from Pandas DataFrame to XLSX sheet: 4215 if xlsx and self.bondsXLSXFile: 4216 with pd.ExcelWriter( 4217 path=self.bondsXLSXFile, 4218 date_format=TKS_DATE_FORMAT, 4219 datetime_format=TKS_DATE_TIME_FORMAT, 4220 mode="w", 4221 ) as writer: 4222 bonds.to_excel( 4223 writer, 4224 sheet_name="Extended bonds data", 4225 index=True, 4226 encoding="UTF-8", 4227 freeze_panes=(1, 1), 4228 ) # saving as XLSX-file with freeze first row and column as headers 4229 4230 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4231 4232 return bonds 4233 4234 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4235 """ 4236 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4237 4238 WARNING! This is too long operation if a lot of bonds requested from broker server. 4239 4240 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4241 4242 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4243 extended information about bonds: main info, current prices, bond payment calendar, 4244 coupon yields, current yields and some statistics etc. 4245 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4246 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4247 for further used by data scientists or stock analytics. 4248 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4249 """ 4250 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4251 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4252 4253 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4254 4255 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4256 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4257 calendar = None 4258 for bond in extBonds.iterrows(): 4259 for item in bond[1]["calendar"]: 4260 cData = { 4261 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4262 "couponDate": item["couponDate"], 4263 "figi": bond[1]["figi"], 4264 "ticker": bond[1]["ticker"], 4265 "name": bond[1]["name"], 4266 "couponNumber": item["couponNumber"], 4267 "payOneBond": item["payOneBond"], 4268 "payCurrency": item["payCurrency"], 4269 "couponType": item["couponType"], 4270 "couponPeriod": item["couponPeriod"], 4271 "fixDate": item["fixDate"], 4272 "couponStartDate": item["couponStartDate"], 4273 "couponEndDate": item["couponEndDate"], 4274 } 4275 4276 if calendar is None: 4277 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4278 4279 else: 4280 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4281 4282 if calendar is not None: 4283 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4284 4285 # Saving calendar from Pandas DataFrame to XLSX sheet: 4286 if xlsx: 4287 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4288 4289 with pd.ExcelWriter( 4290 path=xlsxCalendarFile, 4291 date_format=TKS_DATE_FORMAT, 4292 datetime_format=TKS_DATE_TIME_FORMAT, 4293 mode="w", 4294 ) as writer: 4295 humanReadable = calendar.copy(deep=True) 4296 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4297 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4298 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4299 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4300 humanReadable.columns = colNames # human-readable column names 4301 4302 humanReadable.to_excel( 4303 writer, 4304 sheet_name="Bond payments calendar", 4305 index=False, 4306 encoding="UTF-8", 4307 freeze_panes=(1, 2), 4308 ) # saving as XLSX-file with freeze first row and column as headers 4309 4310 del humanReadable # release df in memory 4311 4312 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4313 4314 return calendar 4315 4316 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4317 """ 4318 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4319 Also, creates Markdown file with calendar data, `calendar.md` by default. 4320 4321 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4322 4323 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4324 extended information about bonds: main info, current prices, bond payment calendar, 4325 coupon yields, current yields and some statistics etc. 4326 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4327 :param show: if `True` then also printing bonds payment calendar to the console, 4328 otherwise save to file `calendarFile` only. `False` by default. 4329 :return: multilines text in Markdown format with bonds payment calendar as a table. 4330 """ 4331 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4332 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4333 4334 infoText = "# Bond payments calendar\n\n" 4335 4336 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4337 4338 if not (calendar is None or calendar.empty): 4339 splitLine = "| | | | | | | | | |\n" 4340 4341 info = [ 4342 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4343 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4344 ] 4345 4346 newMonth = False 4347 notOneBond = calendar["figi"].nunique() > 1 4348 for i, bond in enumerate(calendar.iterrows()): 4349 if newMonth and notOneBond: 4350 info.append(splitLine) 4351 4352 info.append( 4353 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4354 " √" if bond[1]["paid"] else " —", 4355 bond[1]["couponDate"].split("T")[0], 4356 bond[1]["figi"], 4357 bond[1]["ticker"], 4358 bond[1]["couponNumber"], 4359 "{} {}".format( 4360 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4361 bond[1]["payCurrency"], 4362 ), 4363 bond[1]["couponType"], 4364 bond[1]["couponPeriod"], 4365 bond[1]["fixDate"].split("T")[0], 4366 ) 4367 ) 4368 4369 if i < len(calendar.values) - 1: 4370 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4371 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4372 newMonth = False if curDate.month == nextDate.month else True 4373 4374 else: 4375 newMonth = False 4376 4377 infoText += "".join(info) 4378 4379 if show: 4380 uLogger.info("{}".format(infoText)) 4381 4382 if self.calendarFile is not None: 4383 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4384 fH.write(infoText) 4385 4386 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4387 4388 else: 4389 infoText += "No data\n" 4390 4391 return infoText 4392 4393 def OverviewAccounts(self, show: bool = False) -> dict: 4394 """ 4395 Method for parsing and show simple table with all available user accounts. 4396 4397 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4398 4399 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4400 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4401 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4402 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4403 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4404 "closed": "—", "access": "Full access" }, ...}}` 4405 """ 4406 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4407 4408 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4409 accounts = { 4410 item["id"]: { 4411 "type": TKS_ACCOUNT_TYPES[item["type"]], 4412 "name": item["name"], 4413 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4414 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4415 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4416 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4417 } for item in rawAccounts["accounts"] 4418 } 4419 4420 # Raw and parsed data with some fields replaced in "stat" section: 4421 view = { 4422 "rawAccounts": rawAccounts, 4423 "stat": accounts, 4424 } 4425 4426 # --- Prepare simple text table with only accounts data in human-readable format: 4427 if show: 4428 info = [ 4429 "# User accounts\n\n", 4430 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4431 "| Account ID | Type | Status | Name |\n", 4432 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4433 ] 4434 4435 for account in view["stat"].keys(): 4436 info.extend([ 4437 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4438 account, 4439 view["stat"][account]["type"], 4440 view["stat"][account]["status"], 4441 view["stat"][account]["name"], 4442 ) 4443 ]) 4444 4445 infoText = "".join(info) 4446 4447 uLogger.info(infoText) 4448 4449 if self.userAccountsFile: 4450 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4451 fH.write(infoText) 4452 4453 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4454 4455 return view 4456 4457 def OverviewUserInfo(self, show: bool = False) -> dict: 4458 """ 4459 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4460 4461 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4462 4463 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4464 :return: dict with raw parsed data from server and some calculated statistics about it. 4465 """ 4466 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4467 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4468 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4469 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4470 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4471 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4472 4473 # This is dict with parsed common user data: 4474 userInfo = { 4475 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4476 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4477 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4478 "tariff": rawUserInfo["tariff"], 4479 } 4480 4481 # This is an array of dict with parsed margin statuses for every account IDs: 4482 margins = {} 4483 for accountId in accounts.keys(): 4484 if rawMargins[accountId]: 4485 margins[accountId] = { 4486 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4487 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4488 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4489 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4490 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4491 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4492 } 4493 4494 else: 4495 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4496 4497 unary = {} # unary-connection limits 4498 for item in rawTariffLimits["unaryLimits"]: 4499 if item["limitPerMinute"] in unary.keys(): 4500 unary[item["limitPerMinute"]].extend(item["methods"]) 4501 4502 else: 4503 unary[item["limitPerMinute"]] = item["methods"] 4504 4505 stream = {} # stream-connection limits 4506 for item in rawTariffLimits["streamLimits"]: 4507 if item["limit"] in stream.keys(): 4508 stream[item["limit"]].extend(item["streams"]) 4509 4510 else: 4511 stream[item["limit"]] = item["streams"] 4512 4513 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4514 limits = { 4515 "unary": unary, 4516 "stream": stream, 4517 } 4518 4519 # Raw and parsed data as an output result: 4520 view = { 4521 "rawUserInfo": rawUserInfo, 4522 "rawAccounts": rawAccounts, 4523 "rawMargins": rawMargins, 4524 "rawTariffLimits": rawTariffLimits, 4525 "stat": { 4526 "userInfo": userInfo, 4527 "accounts": accounts, 4528 "margins": margins, 4529 "limits": limits, 4530 }, 4531 } 4532 4533 # --- Prepare text table with user information in human-readable format: 4534 if show: 4535 info = [ 4536 "# Full user information\n\n", 4537 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4538 "## Common information\n\n", 4539 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4540 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4541 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4542 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4543 "\n## User accounts\n\n", 4544 ] 4545 4546 for account in view["stat"]["accounts"].keys(): 4547 info.extend([ 4548 "### ID: [{}]\n\n".format(account), 4549 "| Parameters | Values |\n", 4550 "|----------------------|--------------------------------------------------------------|\n", 4551 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4552 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4553 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4554 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4555 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4556 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4557 ]) 4558 4559 if margins[account]: 4560 info.extend([ 4561 "| Margin status: | Enabled |\n", 4562 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4563 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4564 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4565 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4566 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4567 ]) 4568 4569 else: 4570 info.append("| Margin status: | Disabled |\n\n") 4571 4572 info.extend([ 4573 "\n## Current user tariff limits\n", 4574 "\nSee also:\n", 4575 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4576 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4577 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4578 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4579 "\n### Unary limits\n", 4580 ]) 4581 4582 if unary: 4583 for key, values in sorted(unary.items()): 4584 info.append("\n* Max requests per minute: {}\n".format(key)) 4585 4586 for value in values: 4587 info.append(" - {}\n".format(value)) 4588 4589 else: 4590 info.append("\nNot available\n") 4591 4592 info.append("\n### Stream limits\n") 4593 4594 if stream: 4595 for key, values in sorted(stream.items()): 4596 info.append("\n* Max stream connections: {}\n".format(key)) 4597 4598 for value in values: 4599 info.append(" - {}\n".format(value)) 4600 4601 else: 4602 info.append("\nNot available\n") 4603 4604 infoText = "".join(info) 4605 4606 uLogger.info(infoText) 4607 4608 if self.userInfoFile: 4609 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4610 fH.write(infoText) 4611 4612 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4613 4614 return view 4615 4616 4617class Args: 4618 """ 4619 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4620 """ 4621 def __init__(self, **kwargs): 4622 self.__dict__.update(kwargs) 4623 4624 def __getattr__(self, item): 4625 return None 4626 4627 4628def ParseArgs(): 4629 """This function get and parse command line keys.""" 4630 parser = ArgumentParser() # command-line string parser 4631 4632 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4633 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4634 4635 # --- options: 4636 4637 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4638 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4639 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4640 4641 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4642 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4643 4644 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4645 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4646 4647 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4648 4649 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4650 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4651 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4652 4653 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4654 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4655 4656 # --- commands: 4657 4658 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4659 4660 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4661 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4662 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4663 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4664 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4665 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4666 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4667 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4668 4669 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4670 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4671 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4672 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4673 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4674 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4675 4676 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4677 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4678 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4679 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4680 4681 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4682 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4683 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4684 4685 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4686 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4687 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4688 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4689 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4690 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4691 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4692 4693 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4694 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4695 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4696 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4697 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4698 4699 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4700 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4701 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4702 4703 cmdArgs = parser.parse_args() 4704 return cmdArgs 4705 4706 4707def Main(**kwargs): 4708 """ 4709 Main function for work with TKSBrokerAPI in the console. 4710 4711 See examples: 4712 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4713 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4714 """ 4715 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4716 4717 if args.debug_level: 4718 uLogger.level = 10 # always debug level by default 4719 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4720 4721 exitCode = 0 4722 start = datetime.now(tzutc()) 4723 uLogger.debug("=-" * 50) 4724 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4725 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4726 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4727 )) 4728 4729 # trying to calculate full current version: 4730 buildVersion = __version__ 4731 try: 4732 v = version("tksbrokerapi") 4733 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4734 4735 except Exception: 4736 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4737 4738 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4739 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4740 4741 try: 4742 if args.version: 4743 print("TKSBrokerAPI {}".format(buildVersion)) 4744 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4745 4746 else: 4747 # Init class for trading with Tinkoff Broker: 4748 trader = TinkoffBrokerServer( 4749 token=args.token, 4750 accountId=args.account_id, 4751 useCache=not args.no_cache, 4752 ) 4753 4754 # --- set some options: 4755 4756 if args.more: 4757 trader.moreDebug = True 4758 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4759 4760 if args.ticker: 4761 ticker = str(args.ticker).upper() # Tickers may be upper case only 4762 4763 if ticker in trader.aliasesKeys: 4764 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4765 4766 else: 4767 trader.ticker = ticker 4768 4769 if args.figi: 4770 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4771 4772 if args.depth is not None: 4773 trader.depth = args.depth 4774 4775 # --- do one command: 4776 4777 if args.list: 4778 if args.output is not None: 4779 trader.instrumentsFile = args.output 4780 4781 trader.ShowInstrumentsInfo(show=True) 4782 4783 elif args.list_xlsx: 4784 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4785 4786 elif args.bonds_xlsx is not None: 4787 if args.output is not None: 4788 trader.bondsXLSXFile = args.output 4789 4790 if len(args.bonds_xlsx) == 0: 4791 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4792 4793 else: 4794 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4795 4796 elif args.search: 4797 if args.output is not None: 4798 trader.searchResultsFile = args.output 4799 4800 trader.SearchInstruments(pattern=args.search[0], show=True) 4801 4802 elif args.info: 4803 if not (args.ticker or args.figi): 4804 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4805 raise Exception("Ticker or FIGI required") 4806 4807 if args.output is not None: 4808 trader.infoFile = args.output 4809 4810 if args.ticker: 4811 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4812 4813 else: 4814 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4815 4816 elif args.calendar is not None: 4817 if args.output is not None: 4818 trader.calendarFile = args.output 4819 4820 if len(args.calendar) == 0: 4821 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4822 4823 else: 4824 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4825 4826 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4827 4828 elif args.price: 4829 if not (args.ticker or args.figi): 4830 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4831 raise Exception("Ticker or FIGI required") 4832 4833 trader.GetCurrentPrices(show=True) 4834 4835 elif args.prices is not None: 4836 if args.output is not None: 4837 trader.pricesFile = args.output 4838 4839 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4840 4841 elif args.overview: 4842 if args.output is not None: 4843 trader.overviewFile = args.output 4844 4845 trader.Overview(show=True, details="full") 4846 4847 elif args.overview_digest: 4848 if args.output is not None: 4849 trader.overviewDigestFile = args.output 4850 4851 trader.Overview(show=True, details="digest") 4852 4853 elif args.overview_positions: 4854 if args.output is not None: 4855 trader.overviewPositionsFile = args.output 4856 4857 trader.Overview(show=True, details="positions") 4858 4859 elif args.overview_orders: 4860 if args.output is not None: 4861 trader.overviewOrdersFile = args.output 4862 4863 trader.Overview(show=True, details="orders") 4864 4865 elif args.overview_analytics: 4866 if args.output is not None: 4867 trader.overviewAnalyticsFile = args.output 4868 4869 trader.Overview(show=True, details="analytics") 4870 4871 elif args.overview_calendar: 4872 if args.output is not None: 4873 trader.overviewAnalyticsFile = args.output 4874 4875 trader.Overview(show=True, details="calendar") 4876 4877 elif args.deals is not None: 4878 if args.output is not None: 4879 trader.reportFile = args.output 4880 4881 if 0 <= len(args.deals) < 3: 4882 trader.Deals( 4883 start=args.deals[0] if len(args.deals) >= 1 else None, 4884 end=args.deals[1] if len(args.deals) == 2 else None, 4885 show=True, # Always show deals report in console 4886 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4887 ) 4888 4889 else: 4890 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4891 raise Exception("Incorrect value") 4892 4893 elif args.history is not None: 4894 if args.output is not None: 4895 trader.historyFile = args.output 4896 4897 if 0 <= len(args.history) < 3: 4898 dataReceived = trader.History( 4899 start=args.history[0] if len(args.history) >= 1 else None, 4900 end=args.history[1] if len(args.history) == 2 else None, 4901 interval="hour" if args.interval is None or not args.interval else args.interval, 4902 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4903 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4904 show=True, # shows all downloaded candles in console 4905 ) 4906 4907 if args.render_chart is not None and dataReceived is not None: 4908 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4909 4910 trader.ShowHistoryChart( 4911 candles=dataReceived, 4912 interact=iChart, 4913 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4914 ) 4915 4916 else: 4917 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4918 raise Exception("Incorrect value") 4919 4920 elif args.load_history is not None: 4921 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4922 4923 if args.render_chart is not None and histData is not None: 4924 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4925 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4926 4927 trader.ShowHistoryChart( 4928 candles=histData, 4929 interact=iChart, 4930 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4931 ) 4932 4933 elif args.trade is not None: 4934 if 1 <= len(args.trade) <= 5: 4935 trader.Trade( 4936 operation=args.trade[0], 4937 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4938 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4939 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4940 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4941 ) 4942 4943 else: 4944 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4945 4946 elif args.buy is not None: 4947 if 0 <= len(args.buy) <= 4: 4948 trader.Buy( 4949 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4950 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4951 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4952 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4953 ) 4954 4955 else: 4956 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4957 4958 elif args.sell is not None: 4959 if 0 <= len(args.sell) <= 4: 4960 trader.Sell( 4961 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4962 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4963 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4964 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4965 ) 4966 4967 else: 4968 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4969 4970 elif args.order: 4971 if 4 <= len(args.order) <= 7: 4972 trader.Order( 4973 operation=args.order[0], 4974 orderType=args.order[1], 4975 lots=int(args.order[2]), 4976 targetPrice=float(args.order[3]), 4977 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4978 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4979 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4980 ) 4981 4982 else: 4983 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4984 4985 elif args.buy_limit: 4986 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4987 4988 elif args.sell_limit: 4989 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4990 4991 elif args.buy_stop: 4992 if 2 <= len(args.buy_stop) <= 7: 4993 trader.BuyStop( 4994 lots=int(args.buy_stop[0]), 4995 targetPrice=float(args.buy_stop[1]), 4996 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4997 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4998 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4999 ) 5000 5001 else: 5002 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5003 5004 elif args.sell_stop: 5005 if 2 <= len(args.sell_stop) <= 7: 5006 trader.SellStop( 5007 lots=int(args.sell_stop[0]), 5008 targetPrice=float(args.sell_stop[1]), 5009 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5010 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5011 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5012 ) 5013 5014 else: 5015 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5016 5017 # elif args.buy_order_grid is not None: 5018 # # update order grid work with api v2 5019 # if len(args.buy_order_grid) == 2: 5020 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5021 # 5022 # for order in orderParams: 5023 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5024 # 5025 # else: 5026 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5027 # 5028 # elif args.sell_order_grid is not None: 5029 # # update order grid work with api v2 5030 # if len(args.sell_order_grid) >= 2: 5031 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5032 # 5033 # for order in orderParams: 5034 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5035 # 5036 # else: 5037 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5038 5039 elif args.close_order is not None: 5040 trader.CloseOrders(args.close_order) # close only one order 5041 5042 elif args.close_orders is not None: 5043 trader.CloseOrders(args.close_orders) # close list of orders 5044 5045 elif args.close_trade: 5046 if not (args.ticker or args.figi): 5047 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5048 raise Exception("Ticker or FIGI required") 5049 5050 if args.ticker: 5051 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5052 5053 else: 5054 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5055 5056 elif args.close_trades is not None: 5057 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5058 5059 elif args.close_all is not None: 5060 if args.ticker: 5061 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5062 5063 elif args.figi: 5064 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5065 5066 else: 5067 trader.CloseAll(*args.close_all) 5068 5069 elif args.limits: 5070 if args.output is not None: 5071 trader.withdrawalLimitsFile = args.output 5072 5073 trader.OverviewLimits(show=True) 5074 5075 elif args.user_info: 5076 if args.output is not None: 5077 trader.userInfoFile = args.output 5078 5079 trader.OverviewUserInfo(show=True) 5080 5081 elif args.account: 5082 if args.output is not None: 5083 trader.userAccountsFile = args.output 5084 5085 trader.OverviewAccounts(show=True) 5086 5087 else: 5088 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5089 raise Exception("There is no command to execute") 5090 5091 except Exception: 5092 trace = tb.format_exc() 5093 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5094 if e in trace: 5095 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5096 break 5097 5098 uLogger.debug(trace) 5099 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5100 exitCode = 255 # an error occurred, must be open a ticket for this issue 5101 5102 finally: 5103 finish = datetime.now(tzutc()) 5104 5105 if exitCode == 0: 5106 if args.more: 5107 uLogger.debug("All operations were finished success (summary code is 0).") 5108 5109 else: 5110 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5111 os.path.abspath(uLog.defaultLogFile), exitCode, 5112 )) 5113 5114 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5115 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5116 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5117 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5118 )) 5119 uLogger.debug("=-" * 50) 5120 5121 if not kwargs: 5122 sys.exit(exitCode) 5123 5124 else: 5125 return exitCode 5126 5127 5128if __name__ == "__main__": 5129 Main()
76class TinkoffBrokerServer: 77 """ 78 This class implements methods to work with Tinkoff broker server. 79 80 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 81 82 About `token`: https://tinkoff.github.io/investAPI/token/ 83 """ 84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self._ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self._figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """ 349 350 @property 351 def ticker(self) -> str: 352 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 353 354 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 355 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 356 357 See also: `SearchByTicker()`, `SearchInstruments()`. 358 """ 359 return self._ticker 360 361 @ticker.setter 362 def ticker(self, value): 363 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 364 365 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 366 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 367 368 See also: `SearchByTicker()`, `SearchInstruments()`. 369 """ 370 self._ticker = str(value).upper() # Tickers may be upper case only 371 372 @property 373 def figi(self) -> str: 374 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 375 376 See also: `SearchByFIGI()`, `SearchInstruments()`. 377 """ 378 return self._figi 379 380 @figi.setter 381 def figi(self, value): 382 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 383 384 See also: `SearchByFIGI()`, `SearchInstruments()`. 385 """ 386 self._figi = str(value).upper() # FIGI may be upper case only 387 388 def _ParseJSON(self, rawData="{}") -> dict: 389 """ 390 Parse JSON from response string. 391 392 :param rawData: this is a string with JSON-formatted text. 393 :return: JSON (dictionary), parsed from server response string. 394 """ 395 responseJSON = json.loads(rawData) if rawData else {} 396 397 if self.moreDebug: 398 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 399 400 return responseJSON 401 402 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 403 """ 404 Send GET or POST request to broker server and receive JSON object. 405 406 self.header: must be defining with dictionary of headers. 407 self.body: if define then used as request body. None by default. 408 self.timeout: global request timeout, 15 seconds by default. 409 :param url: url with REST request. 410 :param reqType: send "GET" or "POST" request. "GET" by default. 411 :param retry: how many times retry after first request if an 5xx server errors occurred. 412 :param pause: sleep time in seconds between retries. 413 :return: response JSON (dictionary) from broker. 414 """ 415 if reqType.upper() not in ("GET", "POST"): 416 uLogger.error("You can define request type: `GET` or `POST`!") 417 raise Exception("Incorrect value") 418 419 if self.moreDebug: 420 uLogger.debug("Request parameters:") 421 uLogger.debug(" - REST API URL: {}".format(url)) 422 uLogger.debug(" - request type: {}".format(reqType)) 423 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 424 uLogger.debug(" - body:\n{}".format(self.body)) 425 426 # fast hack to avoid all operations with some tickers/FIGI 427 responseJSON = {} 428 oK = True 429 for item in self.exclude: 430 if item in url: 431 if self.moreDebug: 432 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 433 434 oK = False 435 break 436 437 if oK: 438 counter = 0 439 response = None 440 errMsg = "" 441 442 while not response and counter <= retry: 443 if reqType == "GET": 444 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 445 446 if reqType == "POST": 447 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 448 449 if self.moreDebug: 450 uLogger.debug("Response:") 451 uLogger.debug(" - status code: {}".format(response.status_code)) 452 uLogger.debug(" - reason: {}".format(response.reason)) 453 uLogger.debug(" - body length: {}".format(len(response.text))) 454 uLogger.debug(" - headers:\n{}".format(response.headers)) 455 456 # Server returns some headers: 457 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 458 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 459 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 460 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 461 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 462 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 463 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 464 sleep(rateLimitWait) 465 466 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 467 if 400 <= response.status_code < 500: 468 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 469 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 470 471 if "code" in response.text and "message" in response.text: 472 msgDict = self._ParseJSON(rawData=response.text) 473 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 474 475 counter = retry + 1 # do not retry for 4xx errors 476 477 if 500 <= response.status_code < 600: 478 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 479 uLogger.debug(" - not oK, {}".format(errMsg)) 480 481 if "code" in response.text and "message" in response.text: 482 errMsgDict = self._ParseJSON(rawData=response.text) 483 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 484 485 counter += 1 486 487 if counter <= retry: 488 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 489 sleep(pause) 490 491 responseJSON = self._ParseJSON(rawData=response.text) 492 493 if errMsg: 494 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 495 uLogger.error(" - not oK, {}".format(errMsg)) 496 497 return responseJSON 498 499 def _IUpdater(self, iType: str) -> tuple: 500 """ 501 Request instrument by type from server. See available API methods for instruments: 502 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 503 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 504 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 505 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 506 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 507 508 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 509 :return: tuple with iType name and list of available instruments of current type for defined user token. 510 """ 511 result = [] 512 513 if iType in TKS_INSTRUMENTS: 514 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 515 516 # all instruments have the same body in API v2 requests: 517 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 518 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 519 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 520 521 return iType, result 522 523 def _IWrapper(self, kwargs): 524 """ 525 Wrapper runs instrument's update method `_IUpdater()`. 526 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 527 """ 528 return self._IUpdater(**kwargs) 529 530 def Listing(self) -> dict: 531 """ 532 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 533 534 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 535 """ 536 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 537 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 538 539 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 540 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 541 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 542 543 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 544 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 545 poolUpdater.close() 546 547 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 548 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 549 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 550 551 # calculate minimum price increment (step) for all instruments and set up instrument's type: 552 for iType in iList.keys(): 553 for ticker in iList[iType]: 554 iList[iType][ticker]["type"] = iType 555 556 if "minPriceIncrement" in iList[iType][ticker].keys(): 557 iList[iType][ticker]["step"] = NanoToFloat( 558 iList[iType][ticker]["minPriceIncrement"]["units"], 559 iList[iType][ticker]["minPriceIncrement"]["nano"], 560 ) 561 562 else: 563 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 564 565 return iList 566 567 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 568 """ 569 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 570 571 See also: `DumpInstruments()`, `Listing()`. 572 573 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 574 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 575 """ 576 if self.iListDumpFile is None or not self.iListDumpFile: 577 uLogger.error("Output name of dump file must be defined!") 578 raise Exception("Filename required") 579 580 if not self.iList or forceUpdate: 581 self.iList = self.Listing() 582 583 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 584 585 # Save as XLSX with separated sheets for every type of instruments: 586 with pd.ExcelWriter( 587 path=xlsxDumpFile, 588 date_format=TKS_DATE_FORMAT, 589 datetime_format=TKS_DATE_TIME_FORMAT, 590 mode="w", 591 ) as writer: 592 for iType in TKS_INSTRUMENTS: 593 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 594 df = df[sorted(df)] # sorted by column names 595 df = df.applymap( 596 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 597 na_action="ignore", 598 ) # converting numbers from nano-type to float in every cell 599 df.to_excel( 600 writer, 601 sheet_name=iType, 602 encoding="UTF-8", 603 freeze_panes=(1, 1), 604 ) # saving as XLSX-file with freeze first row and column as headers 605 606 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 607 608 def DumpInstruments(self, forceUpdate: bool = True) -> str: 609 """ 610 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 611 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 612 613 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 614 615 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 616 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 617 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 618 """ 619 if self.iListDumpFile is None or not self.iListDumpFile: 620 uLogger.error("Output name of dump file must be defined!") 621 raise Exception("Filename required") 622 623 if not self.iList or forceUpdate: 624 self.iList = self.Listing() 625 626 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 627 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 628 fH.write(jsonDump) 629 630 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 631 632 return jsonDump 633 634 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 635 """ 636 Show information about one instrument defined by json data and prints it in Markdown format. 637 638 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 639 640 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 641 :param show: if `True` then also printing information about instrument and its current price. 642 :return: multilines text in Markdown format with information about one instrument. 643 """ 644 splitLine = "| | |\n" 645 infoText = "" 646 647 if iJSON is not None and iJSON and isinstance(iJSON, dict): 648 info = [ 649 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 650 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 651 "| Parameters | Values |\n", 652 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 653 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 654 "| Full name: | {:<54} |\n".format(iJSON["name"]), 655 ] 656 657 if "sector" in iJSON.keys() and iJSON["sector"]: 658 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 659 660 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 661 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 662 663 info.extend([ 664 splitLine, 665 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 666 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 667 ]) 668 669 if "isin" in iJSON.keys() and iJSON["isin"]: 670 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 671 672 if "classCode" in iJSON.keys(): 673 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 674 675 info.extend([ 676 splitLine, 677 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 678 splitLine, 679 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 680 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 681 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 682 ]) 683 684 if iJSON["figi"]: 685 self._figi = iJSON["figi"] 686 iJSON = iJSON | self.RequestTradingStatus() 687 688 info.extend([ 689 splitLine, 690 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 691 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 692 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 693 ]) 694 695 info.append(splitLine) 696 697 if "type" in iJSON.keys() and iJSON["type"]: 698 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 699 700 if "shareType" in iJSON.keys() and iJSON["shareType"]: 701 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 702 703 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 704 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 705 706 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 707 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 708 709 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 710 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 711 712 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 713 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 714 715 if "focusType" in iJSON.keys() and iJSON["focusType"]: 716 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 717 718 if "assetType" in iJSON.keys() and iJSON["assetType"]: 719 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 720 721 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 722 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 723 724 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 725 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 726 727 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 728 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 729 730 if "currency" in iJSON.keys(): 731 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 732 733 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 734 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 735 736 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 737 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 738 739 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 740 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 741 742 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 743 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 744 745 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 746 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 747 748 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 749 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 750 751 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 752 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 753 754 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 755 info.append("| Perpetual bond: | Yes |\n") 756 757 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 758 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 759 760 iExt = None 761 if iJSON["type"] == "Bonds": 762 info.extend([ 763 splitLine, 764 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 765 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 766 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 767 iJSON["nominal"]["currency"], 768 )), 769 ]) 770 771 if "floatingCouponFlag" in iJSON.keys(): 772 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 773 774 if "amortizationFlag" in iJSON.keys(): 775 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 776 777 info.append(splitLine) 778 779 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 780 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 781 782 if iJSON["figi"]: 783 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 784 785 info.extend([ 786 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 787 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 788 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 789 ]) 790 791 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 792 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 793 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 794 iJSON["aciValue"]["currency"] 795 ))) 796 797 if "currentPrice" in iJSON.keys(): 798 info.append(splitLine) 799 800 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 801 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 802 803 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 804 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 805 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 806 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 807 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 808 809 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 810 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 811 812 info.extend([ 813 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 814 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 815 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 816 )), 817 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 818 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 819 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 820 )), 821 "| Changes between last deal price and last close | {:<54} |\n".format( 822 "{:.2f}%{}".format( 823 iJSON["currentPrice"]["changes"], 824 " ({}{:.2f} {})".format( 825 "+" if bondChangesDelta > 0 else "", 826 bondChangesDelta, 827 aciCurrency 828 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 829 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 830 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 831 currency 832 ), 833 ) 834 ), 835 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 836 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 837 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 838 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 839 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 840 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 841 )), 842 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 843 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 844 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 845 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 846 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 847 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 848 )), 849 ]) 850 851 if "lot" in iJSON.keys(): 852 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 853 854 if "step" in iJSON.keys() and iJSON["step"] != 0: 855 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 856 857 # Add bond payment calendar: 858 if iJSON["type"] == "Bonds": 859 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 860 info.extend(["\n", strCalendar]) 861 862 infoText += "".join(info) 863 864 if show: 865 uLogger.info("{}".format(infoText)) 866 867 else: 868 uLogger.debug("{}".format(infoText)) 869 870 if self.infoFile is not None: 871 with open(self.infoFile, "w", encoding="UTF-8") as fH: 872 fH.write(infoText) 873 874 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 875 876 return infoText 877 878 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 879 """ 880 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 881 882 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 883 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 884 :return: JSON formatted data with information about instrument. 885 """ 886 tickerJSON = {} 887 if self.moreDebug: 888 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 889 890 if not self._ticker: 891 uLogger.warning("self._ticker variable is not be empty!") 892 893 else: 894 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 895 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 896 raise Exception("Instrument not allowed") 897 898 if not self.iList: 899 self.iList = self.Listing() 900 901 if self._ticker in self.iList["Shares"].keys(): 902 tickerJSON = self.iList["Shares"][self._ticker] 903 if self.moreDebug: 904 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 905 906 elif self._ticker in self.iList["Currencies"].keys(): 907 tickerJSON = self.iList["Currencies"][self._ticker] 908 if self.moreDebug: 909 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 910 911 elif self._ticker in self.iList["Bonds"].keys(): 912 tickerJSON = self.iList["Bonds"][self._ticker] 913 if self.moreDebug: 914 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 915 916 elif self._ticker in self.iList["Etfs"].keys(): 917 tickerJSON = self.iList["Etfs"][self._ticker] 918 if self.moreDebug: 919 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 920 921 elif self._ticker in self.iList["Futures"].keys(): 922 tickerJSON = self.iList["Futures"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 925 926 if tickerJSON: 927 self._figi = tickerJSON["figi"] 928 929 if requestPrice: 930 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 931 932 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 933 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 934 935 else: 936 tickerJSON["currentPrice"]["changes"] = 0 937 938 if show: 939 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 940 941 else: 942 if show: 943 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 944 945 return tickerJSON 946 947 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 948 """ 949 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 950 951 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 952 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 953 :return: JSON formatted data with information about instrument. 954 """ 955 figiJSON = {} 956 if self.moreDebug: 957 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 958 959 if not self._figi: 960 uLogger.warning("self._figi variable is not be empty!") 961 962 else: 963 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 964 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 965 raise Exception("Instrument not allowed") 966 967 if not self.iList: 968 self.iList = self.Listing() 969 970 for item in self.iList["Shares"].keys(): 971 if self._figi == self.iList["Shares"][item]["figi"]: 972 figiJSON = self.iList["Shares"][item] 973 974 if self.moreDebug: 975 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 976 977 break 978 979 if not figiJSON: 980 for item in self.iList["Currencies"].keys(): 981 if self._figi == self.iList["Currencies"][item]["figi"]: 982 figiJSON = self.iList["Currencies"][item] 983 984 if self.moreDebug: 985 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 986 987 break 988 989 if not figiJSON: 990 for item in self.iList["Bonds"].keys(): 991 if self._figi == self.iList["Bonds"][item]["figi"]: 992 figiJSON = self.iList["Bonds"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Etfs"].keys(): 1001 if self._figi == self.iList["Etfs"][item]["figi"]: 1002 figiJSON = self.iList["Etfs"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Futures"].keys(): 1011 if self._figi == self.iList["Futures"][item]["figi"]: 1012 figiJSON = self.iList["Futures"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1016 1017 break 1018 1019 if figiJSON: 1020 self._figi = figiJSON["figi"] 1021 self._ticker = figiJSON["ticker"] 1022 1023 if requestPrice: 1024 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1025 1026 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1027 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1028 1029 else: 1030 figiJSON["currentPrice"]["changes"] = 0 1031 1032 if show: 1033 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1034 1035 else: 1036 if show: 1037 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1038 1039 return figiJSON 1040 1041 def GetCurrentPrices(self, show: bool = True) -> dict: 1042 """ 1043 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1044 `{"buy": [{"price": 1243.8, "quantity": 193}, 1045 {"price": 1244.0, "quantity": 168}, 1046 {"price": 1244.8, "quantity": 5}, 1047 {"price": 1245.0, "quantity": 61}, 1048 {"price": 1245.4, "quantity": 60}], 1049 "sell": [{"price": 1243.6, "quantity": 8}, 1050 {"price": 1242.6, "quantity": 10}, 1051 {"price": 1242.4, "quantity": 18}, 1052 {"price": 1242.2, "quantity": 50}, 1053 {"price": 1242.0, "quantity": 113}], 1054 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1055 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1056 - sell: list of dicts with Buyers prices, 1057 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1058 - quantity: volume value by current price in lots, 1059 - limitUp: current trade session limit price, maximum, 1060 - limitDown: current trade session limit price, minimum, 1061 - lastPrice: last deal price of the instrument, 1062 - closePrice: previous trade session close price of the instrument. 1063 1064 See also: `SearchByTicker()` and `SearchByFIGI()`. 1065 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1066 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1067 1068 :param show: if `True` then print DOM to log and console. 1069 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1070 If an error occurred then returns an empty record: 1071 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1072 """ 1073 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1074 1075 if self.depth < 1: 1076 uLogger.error("Depth of Market (DOM) must be >=1!") 1077 raise Exception("Incorrect value") 1078 1079 if not (self._ticker or self._figi): 1080 uLogger.error("self._ticker or self._figi variables must be defined!") 1081 raise Exception("Ticker or FIGI required") 1082 1083 if self._ticker and not self._figi: 1084 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1085 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1086 1087 if not self._ticker and self._figi: 1088 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1089 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1090 1091 if not self._figi: 1092 uLogger.error("FIGI is not defined!") 1093 raise Exception("Ticker or FIGI required") 1094 1095 else: 1096 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1097 1098 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1099 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1100 self.body = str({"figi": self._figi, "depth": self.depth}) 1101 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1102 1103 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1104 # list of dicts with sellers orders: 1105 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1106 1107 # list of dicts with buyers orders: 1108 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1109 1110 # max price of instrument at this time: 1111 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1112 1113 # min price of instrument at this time: 1114 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1115 1116 # last price of deal with instrument: 1117 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1118 1119 # last close price of instrument: 1120 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1121 1122 else: 1123 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1124 uLogger.debug("Server response: {}".format(pricesResponse)) 1125 1126 if show: 1127 if prices["buy"] or prices["sell"]: 1128 info = [ 1129 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1130 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1131 self._ticker, 1132 self._figi, 1133 self.depth, 1134 ), 1135 "-" * 60, "\n", 1136 " Orders of Buyers | Orders of Sellers\n", 1137 "-" * 60, "\n", 1138 " Sell prices (volumes) | Buy prices (volumes)\n", 1139 "-" * 60, "\n", 1140 ] 1141 1142 if not prices["buy"]: 1143 info.append(" | No orders!\n") 1144 sumBuy = 0 1145 1146 else: 1147 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1148 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1149 for item in maxMinSorted: 1150 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1151 1152 if not prices["sell"]: 1153 info.append("No orders! |\n") 1154 sumSell = 0 1155 1156 else: 1157 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1158 for item in prices["sell"]: 1159 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1160 1161 info.extend([ 1162 "-" * 60, "\n", 1163 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1164 "-" * 60, "\n", 1165 ]) 1166 1167 infoText = "".join(info) 1168 1169 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1170 1171 else: 1172 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1173 1174 return prices 1175 1176 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1177 """ 1178 This method get and show information about all available broker instruments for current user account. 1179 If `instrumentsFile` string is not empty then also save information to this file. 1180 1181 :param show: if `True` then print results to console, if `False` — print only to file. 1182 :return: multi-lines string with all available broker instruments 1183 """ 1184 if not self.iList: 1185 self.iList = self.Listing() 1186 1187 info = [ 1188 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1189 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1190 ] 1191 1192 # add instruments count by type: 1193 for iType in self.iList.keys(): 1194 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1195 1196 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1197 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1198 1199 # generating info tables with all instruments by type: 1200 for iType in self.iList.keys(): 1201 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1202 1203 for instrument in self.iList[iType].keys(): 1204 iName = self.iList[iType][instrument]["name"] # instrument's name 1205 if len(iName) > 57: 1206 iName = "{}...".format(iName[:54]) # right trim for a long string 1207 1208 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1209 self.iList[iType][instrument]["ticker"], 1210 iName, 1211 self.iList[iType][instrument]["figi"], 1212 self.iList[iType][instrument]["currency"], 1213 self.iList[iType][instrument]["lot"], 1214 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1215 )) 1216 1217 infoText = "".join(info) 1218 1219 if show: 1220 uLogger.info(infoText) 1221 1222 if self.instrumentsFile: 1223 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1224 fH.write(infoText) 1225 1226 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1227 1228 return infoText 1229 1230 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1231 """ 1232 This method search and show information about instruments by part of its ticker, FIGI or name. 1233 If `searchResultsFile` string is not empty then also save information to this file. 1234 1235 :param pattern: string with part of ticker, FIGI or instrument's name. 1236 :param show: if `True` then print results to console, if `False` — return list of result only. 1237 :return: list of dictionaries with all found instruments. 1238 """ 1239 if not self.iList: 1240 self.iList = self.Listing() 1241 1242 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1243 compiledPattern = re.compile(pattern, re.IGNORECASE) 1244 1245 for iType in self.iList: 1246 for instrument in self.iList[iType].values(): 1247 searchResult = compiledPattern.search(" ".join( 1248 [instrument["ticker"], instrument["figi"], instrument["name"]] 1249 )) 1250 1251 if searchResult: 1252 searchResults[iType][instrument["ticker"]] = instrument 1253 1254 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1255 info = [ 1256 "# Search results\n\n", 1257 "* **Search pattern:** [{}]\n".format(pattern), 1258 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1259 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1260 ] 1261 infoShort = info[:] 1262 1263 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1264 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1265 skippedLine = "| ... | ... | ... | ... |\n" 1266 1267 if resultsLen == 0: 1268 info.append("\nNo results\n") 1269 infoShort.append("\nNo results\n") 1270 uLogger.warning("No results. Try changing your search pattern.") 1271 1272 else: 1273 for iType in searchResults: 1274 iTypeValuesCount = len(searchResults[iType].values()) 1275 if iTypeValuesCount > 0: 1276 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1277 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1278 1279 for instrument in searchResults[iType].values(): 1280 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1281 instrument["type"], 1282 instrument["ticker"], 1283 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1284 instrument["figi"], 1285 )) 1286 1287 if iTypeValuesCount <= 5: 1288 infoShort.extend(info[-iTypeValuesCount:]) 1289 1290 else: 1291 infoShort.extend(info[-5:]) 1292 infoShort.append(skippedLine) 1293 1294 infoText = "".join(info) 1295 infoTextShort = "".join(infoShort) 1296 1297 if show: 1298 uLogger.info(infoTextShort) 1299 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1300 1301 if self.searchResultsFile: 1302 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1303 fH.write(infoText) 1304 1305 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1306 1307 return searchResults 1308 1309 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1310 """ 1311 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1312 1313 :param instruments: list of strings with tickers or FIGIs. 1314 :return: list with unique instrument FIGIs only. 1315 """ 1316 requestedInstruments = [] 1317 for iName in instruments: 1318 if iName not in self.aliases.keys(): 1319 if iName not in requestedInstruments: 1320 requestedInstruments.append(iName) 1321 1322 else: 1323 if iName not in requestedInstruments: 1324 if self.aliases[iName] not in requestedInstruments: 1325 requestedInstruments.append(self.aliases[iName]) 1326 1327 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1328 1329 onlyUniqueFIGIs = [] 1330 for iName in requestedInstruments: 1331 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1332 continue 1333 1334 self._ticker = iName 1335 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1336 1337 if not iData: 1338 self._ticker = "" 1339 self._figi = iName 1340 1341 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1342 1343 if not iData: 1344 self._figi = "" 1345 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1346 1347 if iData and iData["figi"] not in onlyUniqueFIGIs: 1348 onlyUniqueFIGIs.append(iData["figi"]) 1349 1350 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1351 1352 return onlyUniqueFIGIs 1353 1354 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1355 """ 1356 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1357 1358 See limits: https://tinkoff.github.io/investAPI/limits/ 1359 1360 If `pricesFile` string is not empty then also save information to this file. 1361 1362 :param instruments: list of strings with tickers or FIGIs. 1363 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1364 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1365 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1366 """ 1367 if instruments is None or not instruments: 1368 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1369 raise Exception("Ticker or FIGI required") 1370 1371 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1372 1373 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1374 1375 iList = [] # trying to get info and current prices about all unique instruments: 1376 for self._figi in onlyUniqueFIGIs: 1377 iData = self.SearchByFIGI(requestPrice=True) 1378 iList.append(iData) 1379 1380 self.ShowListOfPrices(iList, show) 1381 1382 return iList 1383 1384 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1385 """ 1386 Show table contains current prices of given instruments. 1387 1388 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1389 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1390 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1391 :return: multilines text in Markdown format as a table contains current prices. 1392 """ 1393 infoText = "" 1394 1395 if show or self.pricesFile: 1396 info = [ 1397 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1398 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1399 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1400 ] 1401 1402 for item in iList: 1403 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1404 item["ticker"], 1405 item["figi"], 1406 item["type"], 1407 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1408 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1409 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1410 "{} / {}".format( 1411 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1412 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1413 ), 1414 "{} / {}".format( 1415 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1416 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1417 ), 1418 item["currency"], 1419 )) 1420 1421 infoText = "".join(info) 1422 1423 if show: 1424 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1425 1426 if self.pricesFile: 1427 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1428 fH.write(infoText) 1429 1430 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1431 1432 return infoText 1433 1434 def RequestTradingStatus(self) -> dict: 1435 """ 1436 Requesting trading status for the instrument defined by `figi` variable. 1437 1438 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1439 1440 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1441 1442 :return: dictionary with trading status attributes. Response example: 1443 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1444 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1445 """ 1446 if self._figi is None or not self._figi: 1447 uLogger.error("Variable `figi` must be defined for using this method!") 1448 raise Exception("FIGI required") 1449 1450 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1451 1452 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1453 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1454 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1455 1456 if self.moreDebug: 1457 uLogger.debug("Records about current trading status successfully received") 1458 1459 return tradingStatus 1460 1461 def RequestPortfolio(self) -> dict: 1462 """ 1463 Requesting actual user's portfolio for current `accountId`. 1464 1465 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1466 1467 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1468 1469 :return: dictionary with user's portfolio. 1470 """ 1471 if self.accountId is None or not self.accountId: 1472 uLogger.error("Variable `accountId` must be defined for using this method!") 1473 raise Exception("Account ID required") 1474 1475 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1476 1477 self.body = str({"accountId": self.accountId}) 1478 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1479 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1480 1481 if self.moreDebug: 1482 uLogger.debug("Records about user's portfolio successfully received") 1483 1484 return rawPortfolio 1485 1486 def RequestPositions(self) -> dict: 1487 """ 1488 Requesting open positions by currencies and instruments for current `accountId`. 1489 1490 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1491 1492 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1493 1494 :return: dictionary with open positions by instruments. 1495 """ 1496 if self.accountId is None or not self.accountId: 1497 uLogger.error("Variable `accountId` must be defined for using this method!") 1498 raise Exception("Account ID required") 1499 1500 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1501 1502 self.body = str({"accountId": self.accountId}) 1503 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1504 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1505 1506 if self.moreDebug: 1507 uLogger.debug("Records about current open positions successfully received") 1508 1509 return rawPositions 1510 1511 def RequestPendingOrders(self) -> list: 1512 """ 1513 Requesting current actual pending limit orders for current `accountId`. 1514 1515 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1516 1517 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1518 1519 :return: list of dictionaries with pending limit orders. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1529 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1530 1531 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1532 1533 return rawOrders 1534 1535 def RequestStopOrders(self) -> list: 1536 """ 1537 Requesting current actual stop orders for current `accountId`. 1538 1539 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1540 1541 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1542 1543 :return: list of dictionaries with stop orders. 1544 """ 1545 if self.accountId is None or not self.accountId: 1546 uLogger.error("Variable `accountId` must be defined for using this method!") 1547 raise Exception("Account ID required") 1548 1549 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1550 1551 self.body = str({"accountId": self.accountId}) 1552 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1553 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1554 1555 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1556 1557 return rawStopOrders 1558 1559 def Overview(self, show: bool = False, details: str = "full") -> dict: 1560 """ 1561 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1562 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1563 and `overviewBondsCalendarFile` are defined then also save information to file. 1564 1565 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1566 many requests about the state of the portfolio, and then, based on the received data, a large number 1567 of calculation and statistics are collected. 1568 1569 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1570 :param details: how detailed should the information be? 1571 - `full` — shows full available information about portfolio status (by default), 1572 - `positions` — shows only open positions, 1573 - `orders` — shows only sections of open limits and stop orders. 1574 - `digest` — show a short digest of the portfolio status, 1575 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1576 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1577 :return: dictionary with client's raw portfolio and some statistics. 1578 """ 1579 if self.accountId is None or not self.accountId: 1580 uLogger.error("Variable `accountId` must be defined for using this method!") 1581 raise Exception("Account ID required") 1582 1583 view = { 1584 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1585 "headers": {}, # list of dictionaries, response headers without "positions" section 1586 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1587 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1588 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1589 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1590 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1591 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1592 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1593 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1594 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1595 }, 1596 "stat": { # --- some statistics calculated using "raw" sections: 1597 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1598 "availableRUB": 0., # available rubles (without other currencies) 1599 "blockedRUB": 0., # blocked sum in Russian Rouble 1600 "totalChangesRUB": 0., # changes for all open trades in RUB 1601 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1602 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1603 "sharesCostRUB": 0., # costs of all shares in RUB 1604 "bondsCostRUB": 0., # costs of all bonds in RUB 1605 "etfsCostRUB": 0., # costs of all etfs in RUB 1606 "futuresCostRUB": 0., # costs of all futures in RUB 1607 "Currencies": [], # list of dictionaries of all currencies statistics 1608 "Shares": [], # list of dictionaries of all shares statistics 1609 "Bonds": [], # list of dictionaries of all bonds statistics 1610 "Etfs": [], # list of dictionaries of all etfs statistics 1611 "Futures": [], # list of dictionaries of all futures statistics 1612 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1613 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1614 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1615 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1616 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1617 }, 1618 "analytics": { # --- some analytics of portfolio: 1619 "distrByAssets": {}, # portfolio distribution by assets 1620 "distrByCompanies": {}, # portfolio distribution by companies 1621 "distrBySectors": {}, # portfolio distribution by sectors 1622 "distrByCurrencies": {}, # portfolio distribution by currencies 1623 "distrByCountries": {}, # portfolio distribution by countries 1624 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1625 } 1626 } 1627 1628 details = details.lower() 1629 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1630 if details not in availableDetails: 1631 details = "full" 1632 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1633 1634 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1635 1636 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1637 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1638 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1639 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1640 1641 # save response headers without "positions" section: 1642 for key in portfolioResponse.keys(): 1643 if key != "positions": 1644 view["raw"]["headers"][key] = portfolioResponse[key] 1645 1646 else: 1647 continue 1648 1649 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1650 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1651 for item in portfolioResponse["positions"]: 1652 if item["instrumentType"] == "currency": 1653 self._figi = item["figi"] 1654 curr = self.SearchByFIGI(requestPrice=False) 1655 1656 # current price of currency in RUB: 1657 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1658 "name": curr["name"], 1659 "currentPrice": NanoToFloat( 1660 item["currentPrice"]["units"], 1661 item["currentPrice"]["nano"] 1662 ), 1663 } 1664 1665 view["raw"]["Currencies"].append(item) 1666 1667 elif item["instrumentType"] == "share": 1668 view["raw"]["Shares"].append(item) 1669 1670 elif item["instrumentType"] == "bond": 1671 view["raw"]["Bonds"].append(item) 1672 1673 elif item["instrumentType"] == "etf": 1674 view["raw"]["Etfs"].append(item) 1675 1676 elif item["instrumentType"] == "futures": 1677 view["raw"]["Futures"].append(item) 1678 1679 else: 1680 continue 1681 1682 # how many volume of currencies (by ISO currency name) are blocked: 1683 for item in view["raw"]["positions"]["blocked"]: 1684 blocked = NanoToFloat(item["units"], item["nano"]) 1685 if blocked > 0: 1686 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1687 1688 # how many volume of instruments (by FIGI) are blocked: 1689 for item in view["raw"]["positions"]["securities"]: 1690 blocked = int(item["blocked"]) 1691 if blocked > 0: 1692 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1693 1694 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1695 1696 if "rub" in allBlocked.keys(): 1697 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1698 1699 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1700 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1701 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1702 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1703 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1704 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1705 view["stat"]["portfolioCostRUB"] = sum([ 1706 view["stat"]["allCurrenciesCostRUB"], 1707 view["stat"]["sharesCostRUB"], 1708 view["stat"]["bondsCostRUB"], 1709 view["stat"]["etfsCostRUB"], 1710 view["stat"]["futuresCostRUB"], 1711 ]) 1712 1713 # --- calculating some portfolio statistics: 1714 byComp = {} # distribution by companies 1715 bySect = {} # distribution by sectors 1716 byCurr = {} # distribution by currencies (include RUB) 1717 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1718 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1719 1720 for item in portfolioResponse["positions"]: 1721 self._figi = item["figi"] 1722 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1723 1724 if instrument: 1725 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1726 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1727 1728 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1729 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1730 1731 else: 1732 blocked = 0 1733 1734 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1735 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1736 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1737 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1738 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1739 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1740 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1741 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1742 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1743 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1744 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1745 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1746 1747 statData = { 1748 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1749 "ticker": instrument["ticker"], # ticker by FIGI 1750 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1751 "volume": volume, # available volume of instrument 1752 "lots": lots, # volume in lots of instrument 1753 "direction": direction, # direction of an instrument's position: short or long 1754 "blocked": blocked, # blocked volume of currency or instrument 1755 "currentPrice": curPrice, # current instrument's price in basic asset 1756 "average": average, # current average position price 1757 "cost": cost, # current cost of all volume of instrument in basic asset 1758 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1759 "costRUB": costRUB, # cost of instrument in ruble 1760 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1761 "profit": profit, # expected profit at current moment 1762 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1763 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1764 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1765 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1766 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1767 "step": instrument["step"], # minimum price increment 1768 } 1769 1770 # adding distribution by unique countries: 1771 if statData["country"] not in byCountry.keys(): 1772 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1773 1774 else: 1775 byCountry[statData["country"]]["cost"] += costRUB 1776 byCountry[statData["country"]]["percent"] += percentCostRUB 1777 1778 if item["instrumentType"] != "currency": 1779 # adding distribution by unique companies: 1780 if statData["name"]: 1781 if statData["name"] not in byComp.keys(): 1782 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1783 1784 else: 1785 byComp[statData["name"]]["cost"] += costRUB 1786 byComp[statData["name"]]["percent"] += percentCostRUB 1787 1788 # adding distribution by unique sectors: 1789 if statData["sector"] not in bySect.keys(): 1790 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1791 1792 else: 1793 bySect[statData["sector"]]["cost"] += costRUB 1794 bySect[statData["sector"]]["percent"] += percentCostRUB 1795 1796 # adding distribution by unique currencies: 1797 if currency not in byCurr.keys(): 1798 byCurr[currency] = { 1799 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1800 "cost": costRUB, 1801 "percent": percentCostRUB 1802 } 1803 1804 else: 1805 byCurr[currency]["cost"] += costRUB 1806 byCurr[currency]["percent"] += percentCostRUB 1807 1808 # saving statistics for every instrument: 1809 if item["instrumentType"] == "currency": 1810 view["stat"]["Currencies"].append(statData) 1811 1812 # update dict with free funds for trading (total - blocked) by currencies 1813 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1814 view["stat"]["funds"][currency] = { 1815 "total": volume, 1816 "totalCostRUB": costRUB, # total volume cost in rubles 1817 "free": volume - blocked, 1818 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1819 } 1820 1821 elif item["instrumentType"] == "share": 1822 view["stat"]["Shares"].append(statData) 1823 1824 elif item["instrumentType"] == "bond": 1825 view["stat"]["Bonds"].append(statData) 1826 1827 elif item["instrumentType"] == "etf": 1828 view["stat"]["Etfs"].append(statData) 1829 1830 elif item["instrumentType"] == "Futures": 1831 view["stat"]["Futures"].append(statData) 1832 1833 else: 1834 continue 1835 1836 # total changes in Russian Ruble: 1837 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1838 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1839 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1840 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1841 view["stat"]["funds"]["rub"] = { 1842 "total": view["stat"]["availableRUB"], 1843 "totalCostRUB": view["stat"]["availableRUB"], 1844 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1845 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1846 } 1847 1848 # --- pending limit orders sector data: 1849 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1850 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1851 1852 for item in view["raw"]["orders"]: 1853 self._figi = item["figi"] 1854 1855 if item["figi"] not in uniquePendingOrdersFIGIs: 1856 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1857 1858 uniquePendingOrdersFIGIs.append(item["figi"]) 1859 uniquePendingOrders[item["figi"]] = instrument 1860 1861 else: 1862 instrument = uniquePendingOrders[item["figi"]] 1863 1864 if instrument: 1865 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1866 orderType = TKS_ORDER_TYPES[item["orderType"]] 1867 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1868 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1869 1870 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1871 if item["direction"] == "ORDER_DIRECTION_BUY": 1872 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1873 1874 else: 1875 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1876 1877 # requested price for order execution: 1878 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1879 1880 # necessary changes in percent to reach target from current price: 1881 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1882 1883 view["stat"]["orders"].append({ 1884 "orderID": item["orderId"], # orderId number parameter of current order 1885 "figi": item["figi"], # FIGI identification 1886 "ticker": instrument["ticker"], # ticker name by FIGI 1887 "lotsRequested": item["lotsRequested"], # requested lots value 1888 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1889 "currentPrice": lastPrice, # current instrument's price for defined action 1890 "targetPrice": target, # requested price for order execution in base currency 1891 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1892 "percentChanges": changes, # changes in percent to target from current price 1893 "currency": item["currency"], # instrument's currency name 1894 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1895 "type": orderType, # type of order from TKS_ORDER_TYPES 1896 "status": orderState, # order status from TKS_ORDER_STATES 1897 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1898 }) 1899 1900 # --- stop orders sector data: 1901 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1902 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1903 1904 for item in view["raw"]["stopOrders"]: 1905 self._figi = item["figi"] 1906 1907 if item["figi"] not in uniqueStopOrdersFIGIs: 1908 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1909 1910 uniqueStopOrdersFIGIs.append(item["figi"]) 1911 uniqueStopOrders[item["figi"]] = instrument 1912 1913 else: 1914 instrument = uniqueStopOrders[item["figi"]] 1915 1916 if instrument: 1917 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1918 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1919 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1920 1921 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1922 if "expirationTime" in item.keys(): 1923 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1924 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1925 1926 else: 1927 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1928 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1929 1930 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1931 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1932 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1933 1934 else: 1935 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1936 1937 # requested price when stop-order executed: 1938 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1939 1940 # price for limit-order, set up when stop-order executed: 1941 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1942 1943 # necessary changes in percent to reach target from current price: 1944 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1945 1946 view["stat"]["stopOrders"].append({ 1947 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1948 "figi": item["figi"], # FIGI identification 1949 "ticker": instrument["ticker"], # ticker name by FIGI 1950 "lotsRequested": item["lotsRequested"], # requested lots value 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for stop-order execution in base currency 1953 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1954 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1955 "percentChanges": changes, # changes in percent to target from current price 1956 "currency": item["currency"], # instrument's currency name 1957 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1958 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1959 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1960 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1961 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1962 }) 1963 1964 # --- calculating data for analytics section: 1965 # portfolio distribution by assets: 1966 view["analytics"]["distrByAssets"] = { 1967 "Ruble": { 1968 "uniques": 1, 1969 "cost": view["stat"]["availableRUB"], 1970 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1971 }, 1972 "Currencies": { 1973 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1974 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1975 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1976 }, 1977 "Shares": { 1978 "uniques": len(view["stat"]["Shares"]), 1979 "cost": view["stat"]["sharesCostRUB"], 1980 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1981 }, 1982 "Bonds": { 1983 "uniques": len(view["stat"]["Bonds"]), 1984 "cost": view["stat"]["bondsCostRUB"], 1985 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1986 }, 1987 "Etfs": { 1988 "uniques": len(view["stat"]["Etfs"]), 1989 "cost": view["stat"]["etfsCostRUB"], 1990 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1991 }, 1992 "Futures": { 1993 "uniques": len(view["stat"]["Futures"]), 1994 "cost": view["stat"]["futuresCostRUB"], 1995 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1996 }, 1997 } 1998 1999 # portfolio distribution by companies: 2000 view["analytics"]["distrByCompanies"]["All money cash"] = { 2001 "ticker": "", 2002 "cost": view["stat"]["allCurrenciesCostRUB"], 2003 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 } 2005 view["analytics"]["distrByCompanies"].update(byComp) 2006 2007 # portfolio distribution by sectors: 2008 view["analytics"]["distrBySectors"]["All money cash"] = { 2009 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2010 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2011 } 2012 view["analytics"]["distrBySectors"].update(bySect) 2013 2014 # portfolio distribution by currencies: 2015 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2016 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2017 2018 if self.moreDebug: 2019 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2020 2021 view["analytics"]["distrByCurrencies"].update(byCurr) 2022 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2023 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2024 2025 # portfolio distribution by countries: 2026 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2027 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2028 2029 if self.moreDebug: 2030 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2031 2032 view["analytics"]["distrByCountries"].update(byCountry) 2033 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2034 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2035 2036 # --- Prepare text statistics overview in human-readable: 2037 if show: 2038 # Whatever the value `details`, header not changes: 2039 info = [ 2040 "# Client's portfolio\n\n", 2041 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2042 "* **Account ID:** [{}]\n".format(self.accountId), 2043 ] 2044 2045 if details in ["full", "positions", "digest"]: 2046 info.extend([ 2047 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2048 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2049 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2050 view["stat"]["totalChangesRUB"], 2051 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2052 view["stat"]["totalChangesPercentRUB"], 2053 ), 2054 ]) 2055 2056 if details in ["full", "positions"]: 2057 info.extend([ 2058 "## Open positions\n\n", 2059 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2060 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2061 "| Ruble | {:>31} | | | | | |\n".format( 2062 "{:.2f} ({:.2f}) rub".format( 2063 view["stat"]["availableRUB"], 2064 view["stat"]["blockedRUB"], 2065 ) 2066 ) 2067 ]) 2068 2069 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2070 return [ 2071 "| | | | | | | |\n", 2072 "| {:<27} | | | | | {:>19} | |\n".format( 2073 noTradeStr if noTradeStr else typeStr, 2074 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2075 ), 2076 ] 2077 2078 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2079 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2080 "{} [{}]".format(data["ticker"], data["figi"]), 2081 "{:.2f} ({:.2f}) {}".format( 2082 data["volume"], 2083 data["blocked"], 2084 data["currency"], 2085 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2086 data["volume"], 2087 data["blocked"], 2088 ), 2089 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2090 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2092 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2093 "{}{:.2f} {} ({}{:.2f}%)".format( 2094 "+" if data["profit"] > 0 else "", 2095 data["profit"], data["baseCurrencyName"], 2096 "+" if data["percentProfit"] > 0 else "", 2097 data["percentProfit"], 2098 ), 2099 ) 2100 2101 # --- Show currencies section: 2102 if view["stat"]["Currencies"]: 2103 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2104 for item in view["stat"]["Currencies"]: 2105 info.append(_InfoStr(item, showCurrencyName=True)) 2106 2107 else: 2108 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2109 2110 # --- Show shares section: 2111 if view["stat"]["Shares"]: 2112 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2113 2114 for item in view["stat"]["Shares"]: 2115 info.append(_InfoStr(item)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2119 2120 # --- Show bonds section: 2121 if view["stat"]["Bonds"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2123 2124 for item in view["stat"]["Bonds"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2129 2130 # --- Show etfs section: 2131 if view["stat"]["Etfs"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2133 2134 for item in view["stat"]["Etfs"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2139 2140 # --- Show futures section: 2141 if view["stat"]["Futures"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2143 2144 for item in view["stat"]["Futures"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2149 2150 if details in ["full", "orders"]: 2151 # --- Show pending limit orders section: 2152 if view["stat"]["orders"]: 2153 info.extend([ 2154 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2155 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2156 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2157 ]) 2158 2159 for item in view["stat"]["orders"]: 2160 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2161 "{} [{}]".format(item["ticker"], item["figi"]), 2162 item["orderID"], 2163 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2164 "{} {} ({}{:.2f}%)".format( 2165 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2166 item["baseCurrencyName"], 2167 "+" if item["percentChanges"] > 0 else "", 2168 float(item["percentChanges"]), 2169 ), 2170 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2171 item["action"], 2172 item["type"], 2173 item["date"], 2174 )) 2175 2176 else: 2177 info.append("\n## Total pending limit-orders: 0\n") 2178 2179 # --- Show stop orders section: 2180 if view["stat"]["stopOrders"]: 2181 info.extend([ 2182 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2183 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2184 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2185 ]) 2186 2187 for item in view["stat"]["stopOrders"]: 2188 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2189 "{} [{}]".format(item["ticker"], item["figi"]), 2190 item["orderID"], 2191 item["lotsRequested"], 2192 "{} {} ({}{:.2f}%)".format( 2193 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2194 item["baseCurrencyName"], 2195 "+" if item["percentChanges"] > 0 else "", 2196 float(item["percentChanges"]), 2197 ), 2198 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2199 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2200 item["action"], 2201 item["type"], 2202 item["expType"], 2203 item["createDate"], 2204 item["expDate"], 2205 )) 2206 2207 else: 2208 info.append("\n## Total stop-orders: 0\n") 2209 2210 if details in ["full", "analytics"]: 2211 # -- Show analytics section: 2212 if view["stat"]["portfolioCostRUB"] > 0: 2213 info.extend([ 2214 "\n# Analytics\n" 2215 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2216 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2217 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2218 view["stat"]["totalChangesRUB"], 2219 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2220 view["stat"]["totalChangesPercentRUB"], 2221 ), 2222 "\n## Portfolio distribution by assets\n" 2223 "\n| Type | Uniques | Percent | Current cost |\n", 2224 "|------------------------------------|---------|---------|--------------------|\n", 2225 ]) 2226 2227 for key in view["analytics"]["distrByAssets"].keys(): 2228 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2229 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2230 key, 2231 view["analytics"]["distrByAssets"][key]["uniques"], 2232 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2234 )) 2235 2236 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2237 2238 info.extend([ 2239 "\n## Portfolio distribution by companies\n" 2240 "\n| Company | Percent | Current cost |\n", 2241 aSepLine, 2242 ]) 2243 2244 for company in view["analytics"]["distrByCompanies"].keys(): 2245 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2246 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2247 "{}{}".format( 2248 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2249 company, 2250 ), 2251 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2252 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2253 )) 2254 2255 info.extend([ 2256 "\n## Portfolio distribution by sectors\n" 2257 "\n| Sector | Percent | Current cost |\n", 2258 aSepLine, 2259 ]) 2260 2261 for sector in view["analytics"]["distrBySectors"].keys(): 2262 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2263 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2264 sector, 2265 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2266 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2267 )) 2268 2269 info.extend([ 2270 "\n## Portfolio distribution by currencies\n" 2271 "\n| Instruments currencies | Percent | Current cost |\n", 2272 aSepLine, 2273 ]) 2274 2275 for curr in view["analytics"]["distrByCurrencies"].keys(): 2276 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2277 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2278 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2279 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2280 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2281 )) 2282 2283 info.extend([ 2284 "\n## Portfolio distribution by countries\n" 2285 "\n| Assets by country | Percent | Current cost |\n", 2286 aSepLine, 2287 ]) 2288 2289 for country in view["analytics"]["distrByCountries"].keys(): 2290 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2291 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2292 country, 2293 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2294 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2295 )) 2296 2297 if details in ["full", "calendar"]: 2298 # -- Show bonds payment calendar section: 2299 if view["stat"]["Bonds"]: 2300 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2301 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2302 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2303 2304 else: 2305 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2306 2307 infoText = "".join(info) 2308 2309 uLogger.info(infoText) 2310 2311 if details == "full" and self.overviewFile: 2312 filename = self.overviewFile 2313 2314 elif details == "digest" and self.overviewDigestFile: 2315 filename = self.overviewDigestFile 2316 2317 elif details == "positions" and self.overviewPositionsFile: 2318 filename = self.overviewPositionsFile 2319 2320 elif details == "orders" and self.overviewOrdersFile: 2321 filename = self.overviewOrdersFile 2322 2323 elif details == "analytics" and self.overviewAnalyticsFile: 2324 filename = self.overviewAnalyticsFile 2325 2326 elif details == "calendar" and self.overviewBondsCalendarFile: 2327 filename = self.overviewBondsCalendarFile 2328 2329 else: 2330 filename = "" 2331 2332 if filename: 2333 with open(filename, "w", encoding="UTF-8") as fH: 2334 fH.write(infoText) 2335 2336 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2337 2338 return view 2339 2340 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2341 """ 2342 Returns history operations between two given dates for current `accountId`. 2343 If `reportFile` string is not empty then also save human-readable report. 2344 Shows some statistical data of closed positions. 2345 2346 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2347 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2348 :param show: if `True` then also prints all records to the console. 2349 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2350 :return: original list of dictionaries with history of deals records from API ("operations" key): 2351 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2352 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2353 """ 2354 if self.accountId is None or not self.accountId: 2355 uLogger.error("Variable `accountId` must be defined for using this method!") 2356 raise Exception("Account ID required") 2357 2358 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2359 2360 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2361 2362 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2363 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2364 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2365 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2366 customStat = {} # custom statistics in additional to responseJSON 2367 2368 # --- output report in human-readable format: 2369 if show or self.reportFile: 2370 splitLine1 = "| | | | | |\n" # Summary section 2371 splitLine2 = "| | | | | | | | |\n" # Operations section 2372 nextDay = "" 2373 2374 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2375 2376 if len(ops) > 0: 2377 customStat = { 2378 "opsCount": 0, # total operations count 2379 "buyCount": 0, # buy operations 2380 "sellCount": 0, # sell operations 2381 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2382 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2383 "payIn": {"rub": 0.}, # Deposit brokerage account 2384 "payOut": {"rub": 0.}, # Withdrawals 2385 "divs": {"rub": 0.}, # Dividends income 2386 "coupons": {"rub": 0.}, # Coupon's income 2387 "brokerCom": {"rub": 0.}, # Service commissions 2388 "serviceCom": {"rub": 0.}, # Service commissions 2389 "marginCom": {"rub": 0.}, # Margin commissions 2390 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2391 } 2392 2393 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2394 for item in ops: 2395 if item["state"] == "OPERATION_STATE_EXECUTED": 2396 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2397 2398 # count buy operations: 2399 if "_BUY" in item["operationType"]: 2400 customStat["buyCount"] += 1 2401 2402 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2403 customStat["buyTotal"][item["payment"]["currency"]] += payment 2404 2405 else: 2406 customStat["buyTotal"][item["payment"]["currency"]] = payment 2407 2408 # count sell operations: 2409 elif "_SELL" in item["operationType"]: 2410 customStat["sellCount"] += 1 2411 2412 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2413 customStat["sellTotal"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["sellTotal"][item["payment"]["currency"]] = payment 2417 2418 # count incoming operations: 2419 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2420 if item["payment"]["currency"] in customStat["payIn"].keys(): 2421 customStat["payIn"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["payIn"][item["payment"]["currency"]] = payment 2425 2426 # count withdrawals operations: 2427 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2428 if item["payment"]["currency"] in customStat["payOut"].keys(): 2429 customStat["payOut"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["payOut"][item["payment"]["currency"]] = payment 2433 2434 # count dividends income: 2435 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2436 if item["payment"]["currency"] in customStat["divs"].keys(): 2437 customStat["divs"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["divs"][item["payment"]["currency"]] = payment 2441 2442 # count coupon's income: 2443 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2444 if item["payment"]["currency"] in customStat["coupons"].keys(): 2445 customStat["coupons"][item["payment"]["currency"]] += payment 2446 2447 else: 2448 customStat["coupons"][item["payment"]["currency"]] = payment 2449 2450 # count broker commissions: 2451 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2452 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2453 customStat["brokerCom"][item["payment"]["currency"]] += payment 2454 2455 else: 2456 customStat["brokerCom"][item["payment"]["currency"]] = payment 2457 2458 # count service commissions: 2459 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2460 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2461 customStat["serviceCom"][item["payment"]["currency"]] += payment 2462 2463 else: 2464 customStat["serviceCom"][item["payment"]["currency"]] = payment 2465 2466 # count margin commissions: 2467 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2468 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2469 customStat["marginCom"][item["payment"]["currency"]] += payment 2470 2471 else: 2472 customStat["marginCom"][item["payment"]["currency"]] = payment 2473 2474 # count withholding taxes: 2475 elif "_TAX" in item["operationType"]: 2476 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2477 customStat["allTaxes"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["allTaxes"][item["payment"]["currency"]] = payment 2481 2482 else: 2483 continue 2484 2485 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2486 2487 # --- view "Actions" lines: 2488 info.extend([ 2489 "| Report sections | | | | |\n", 2490 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2491 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2492 "| | Buy: {:<22} | {:<28} | | |\n".format( 2493 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2494 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2495 ), 2496 "| | Sell: {:<21} | {:<28} | | |\n".format( 2497 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2498 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2499 ), 2500 ]) 2501 2502 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2503 for key in opsKeys: 2504 if key == "rub": 2505 continue 2506 2507 info.extend([ 2508 "| | | {:<28} | | |\n".format( 2509 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2510 ), 2511 "| | | {:<28} | | |\n".format( 2512 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2513 ), 2514 ]) 2515 2516 info.append(splitLine1) 2517 2518 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2519 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2520 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2521 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2523 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2524 ) 2525 2526 # --- view "Payments" lines: 2527 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2528 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2529 2530 for key in paymentsKeys: 2531 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2532 2533 info.append(splitLine1) 2534 2535 # --- view "Commissions and taxes" lines: 2536 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2537 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2538 2539 for key in comKeys: 2540 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2541 2542 info.append(splitLine1) 2543 2544 info.extend([ 2545 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2546 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2547 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2548 ]) 2549 2550 else: 2551 info.append("Broker returned no operations during this period\n") 2552 2553 # --- view "Operations" section: 2554 for item in ops: 2555 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2556 continue 2557 2558 else: 2559 self._figi = item["figi"] if item["figi"] else "" 2560 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2561 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2562 2563 # group of deals during one day: 2564 if nextDay and item["date"].split("T")[0] != nextDay: 2565 info.append(splitLine2) 2566 nextDay = "" 2567 2568 else: 2569 nextDay = item["date"].split("T")[0] # saving current day for splitting 2570 2571 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2572 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2573 self._figi if self._figi else "—", 2574 instrument["ticker"] if instrument else "—", 2575 instrument["type"] if instrument else "—", 2576 item["quantity"] if int(item["quantity"]) > 0 else "—", 2577 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2578 TKS_OPERATION_STATES[item["state"]], 2579 TKS_OPERATION_TYPES[item["operationType"]], 2580 )) 2581 2582 infoText = "".join(info) 2583 2584 if show: 2585 if self.moreDebug: 2586 uLogger.debug("Records about history of a client's operations successfully received") 2587 2588 uLogger.info(infoText) 2589 2590 if self.reportFile: 2591 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2592 fH.write(infoText) 2593 2594 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2595 2596 return ops, customStat 2597 2598 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2599 """ 2600 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2601 2602 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2603 Warning! Broker server used ISO UTC time by default. 2604 2605 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2606 Also, `historyFile` used to update history with `onlyMissing` parameter. 2607 2608 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2609 2610 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2611 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2612 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2613 `"hour"`, `"day"`. Default: `"hour"`. 2614 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2615 False by default. Warning! History appends only from last candle to current time 2616 with always update last candle! 2617 :param csvSep: separator if csv-file is used, `,` by default. 2618 :param show: if `True` then also prints Pandas DataFrame to the console. 2619 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2620 `["date", "time", "open", "high", "low", "close", "volume"]`. 2621 """ 2622 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2623 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2624 history = None # empty pandas object for history 2625 2626 if interval not in TKS_CANDLE_INTERVALS.keys(): 2627 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2628 raise Exception("Incorrect value") 2629 2630 if not (self._ticker or self._figi): 2631 uLogger.error("Ticker or FIGI must be defined!") 2632 raise Exception("Ticker or FIGI required") 2633 2634 if self._ticker and not self._figi: 2635 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2636 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2637 2638 if self._figi and not self._ticker: 2639 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2640 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2641 2642 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2643 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2644 if interval.lower() != "day": 2645 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2646 2647 delta = dtEnd - dtStart # current UTC time minus last time in file 2648 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2649 2650 # calculate history length in candles: 2651 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2652 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2653 length += 1 # to avoid fraction time 2654 2655 # calculate data blocks count: 2656 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2657 2658 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2659 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2660 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2661 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2662 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2663 2664 tempOld = None # pandas object for old history, if --only-missing key present 2665 lastTime = None # datetime object of last old candle in file 2666 2667 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2668 uLogger.debug("--only-missing key present, add only last missing candles...") 2669 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2670 2671 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2672 2673 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2674 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2675 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2676 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2677 2678 # get last datetime object from last string in file or minus 1 delta if file is empty: 2679 if len(tempOld) > 0: 2680 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2681 2682 else: 2683 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2684 2685 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2686 2687 responseJSONs = [] # raw history blocks of data 2688 2689 blockEnd = dtEnd 2690 for item in range(blocks): 2691 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2692 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2693 2694 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2695 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2696 )) 2697 2698 if blockStart == blockEnd: 2699 uLogger.debug("Skipped this zero-length block...") 2700 2701 else: 2702 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2703 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2704 self.body = str({ 2705 "figi": self._figi, 2706 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2707 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2708 "interval": TKS_CANDLE_INTERVALS[interval][0] 2709 }) 2710 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2711 2712 if "code" in responseJSON.keys(): 2713 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2714 2715 else: 2716 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2717 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2718 2719 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2720 2721 blockEnd = blockStart 2722 2723 printCount = len(responseJSONs) # candles to show in console 2724 if responseJSONs: 2725 tempHistory = pd.DataFrame( 2726 data={ 2727 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2728 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2729 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2730 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2731 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2732 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2733 "volume": [int(item["volume"]) for item in responseJSONs], 2734 }, 2735 index=range(len(responseJSONs)), 2736 columns=["date", "time", "open", "high", "low", "close", "volume"], 2737 ) 2738 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2739 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2740 2741 # append only newest candles to old history if --only-missing key present: 2742 if onlyMissing and tempOld is not None and lastTime is not None: 2743 index = 0 # find start index in tempHistory data: 2744 2745 for i, item in tempHistory.iterrows(): 2746 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2747 2748 if curTime == lastTime: 2749 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2750 index = i 2751 printCount = index + 1 2752 break 2753 2754 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2755 2756 else: 2757 history = tempHistory # if no `--only-missing` key then load full data from server 2758 2759 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2760 2761 if history is not None and not history.empty: 2762 if show: 2763 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2764 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2765 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2766 )) 2767 2768 else: 2769 uLogger.warning("Received an empty candles history!") 2770 2771 if self.historyFile is not None: 2772 if history is not None and not history.empty: 2773 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2774 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2775 2776 else: 2777 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2778 2779 else: 2780 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2781 2782 return history 2783 2784 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2785 """ 2786 Load candles history from csv-file and return Pandas DataFrame object. 2787 2788 See also: `History()` and `ShowHistoryChart()` methods. 2789 2790 :param filePath: path to csv-file to open. 2791 """ 2792 loadedHistory = None # init candles data object 2793 2794 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2795 2796 if os.path.exists(filePath): 2797 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2798 2799 tfStr = self.priceModel.FormattedDelta( 2800 self.priceModel.timeframe, 2801 "{days} days {hours}h {minutes}m {seconds}s", 2802 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2803 self.priceModel.timeframe, 2804 "{hours}h {minutes}m {seconds}s", 2805 ) 2806 2807 if loadedHistory is not None and not loadedHistory.empty: 2808 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2809 len(loadedHistory), 2810 tfStr, 2811 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2812 ) 2813 2814 else: 2815 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2816 2817 else: 2818 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2819 2820 return loadedHistory 2821 2822 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2823 """ 2824 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2825 2826 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2827 Default: `index.html` (both for interact and non-interact candlesticks chart). 2828 2829 See also: `History()` and `LoadHistory()` methods. 2830 2831 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2832 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2833 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2834 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2835 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2836 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2837 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2838 """ 2839 if isinstance(candles, str): 2840 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2841 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2842 2843 elif isinstance(candles, pd.DataFrame): 2844 self.priceModel.prices = candles # set candles chain from variable 2845 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2846 2847 if "datetime" not in candles.columns: 2848 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2849 2850 else: 2851 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2852 raise Exception("Incorrect value") 2853 2854 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2855 2856 if interact: 2857 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2858 2859 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2860 2861 else: 2862 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2863 2864 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2865 2866 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2867 2868 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2869 """ 2870 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2871 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2872 2873 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2874 2875 :param operation: string "Buy" or "Sell". 2876 :param lots: volume, integer count of lots >= 1. 2877 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2878 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2879 :param expDate: string "Undefined" by default or local date in future, 2880 it is a string with format `%Y-%m-%d %H:%M:%S`. 2881 :return: JSON with response from broker server. 2882 """ 2883 if self.accountId is None or not self.accountId: 2884 uLogger.error("Variable `accountId` must be defined for using this method!") 2885 raise Exception("Account ID required") 2886 2887 if operation is None or not operation or operation not in ("Buy", "Sell"): 2888 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2889 raise Exception("Incorrect value") 2890 2891 if lots is None or lots < 1: 2892 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2893 lots = 1 2894 2895 if tp is None or tp < 0: 2896 tp = 0 2897 2898 if sl is None or sl < 0: 2899 sl = 0 2900 2901 if expDate is None or not expDate: 2902 expDate = "Undefined" 2903 2904 if not (self._ticker or self._figi): 2905 uLogger.error("Ticker or FIGI must be defined!") 2906 raise Exception("Ticker or FIGI required") 2907 2908 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2909 self._ticker = instrument["ticker"] 2910 self._figi = instrument["figi"] 2911 2912 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2913 2914 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2915 self.body = str({ 2916 "figi": self._figi, 2917 "quantity": str(lots), 2918 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2919 "accountId": str(self.accountId), 2920 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2921 }) 2922 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2923 2924 if "orderId" in response.keys(): 2925 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2926 operation, response["orderId"], 2927 self._ticker, self._figi, lots, 2928 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2929 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2930 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2931 )) 2932 2933 if tp > 0: 2934 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2935 2936 if sl > 0: 2937 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2938 2939 else: 2940 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2941 2942 return response 2943 2944 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2945 """ 2946 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2947 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2948 2949 See also: `Order()` and `Trade()` docstrings. 2950 2951 :param lots: volume, integer count of lots >= 1. 2952 :param tp: float > 0, take profit price of stop-order. 2953 :param sl: float > 0, stop loss price of stop-order. 2954 :param expDate: it's a local date in future. 2955 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2956 :return: JSON with response from broker server. 2957 """ 2958 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2959 2960 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2961 """ 2962 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2963 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2964 2965 See also: `Order()` and `Trade()` docstrings. 2966 2967 :param lots: volume, integer count of lots >= 1. 2968 :param tp: float > 0, take profit price of stop-order. 2969 :param sl: float > 0, stop loss price of stop-order. 2970 :param expDate: it's a local date in the future. 2971 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2972 :return: JSON with response from broker server. 2973 """ 2974 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2975 2976 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2977 """ 2978 Close position of given instruments. 2979 2980 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2981 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2982 This avoids unnecessary downloading data from the server. 2983 """ 2984 if instruments is None or not instruments: 2985 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2986 raise Exception("Ticker or FIGI required") 2987 2988 if isinstance(instruments, str): 2989 instruments = [instruments] 2990 2991 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2992 if uniqueInstruments: 2993 if portfolio is None or not portfolio: 2994 portfolio = self.Overview(show=False) 2995 2996 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2997 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2998 2999 for self._figi in uniqueInstruments: 3000 if self._figi not in allOpened: 3001 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3002 continue 3003 3004 # search open trade info about instrument by ticker: 3005 instrument = {} 3006 for iType in TKS_INSTRUMENTS: 3007 if instrument: 3008 break 3009 3010 for item in portfolio["stat"][iType]: 3011 if item["figi"] == self._figi: 3012 instrument = item 3013 break 3014 3015 if instrument: 3016 self._ticker = instrument["ticker"] 3017 self._figi = instrument["figi"] 3018 3019 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3020 self._ticker, 3021 self._figi, 3022 int(instrument["volume"]), 3023 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3024 )) 3025 3026 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3027 3028 if tradeLots > 0: 3029 if instrument["blocked"] > 0: 3030 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3031 instrument["blocked"], 3032 self._ticker, 3033 tradeLots, 3034 )) 3035 3036 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3037 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3038 3039 else: 3040 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3041 3042 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3043 """ 3044 Close all positions of given instruments with defined type. 3045 3046 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3047 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3048 This avoids unnecessary downloading data from the server. 3049 """ 3050 if iType not in TKS_INSTRUMENTS: 3051 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3052 3053 else: 3054 if portfolio is None or not portfolio: 3055 portfolio = self.Overview(show=False) 3056 3057 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3058 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3059 3060 if tickers and portfolio: 3061 self.CloseTrades(tickers, portfolio) 3062 3063 else: 3064 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3065 3066 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3067 """ 3068 Universal method to create market or limit orders with all available parameters for current `accountId`. 3069 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3070 3071 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3072 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3073 3074 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3075 then broker immediately open market order as you can do simple --buy or --sell operations! 3076 3077 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3078 When current price will go up or down to target price value then broker opens a limit order. 3079 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3080 3081 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3082 3083 :param operation: string "Buy" or "Sell". 3084 :param orderType: string "Limit" or "Stop". 3085 :param lots: volume, integer count of lots >= 1. 3086 :param targetPrice: target price > 0. This is open trade price for limit order. 3087 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3088 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3089 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3090 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3091 Stop loss order always executed by market price. 3092 :param expDate: string "Undefined" by default or local date in future. 3093 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3094 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3095 A limit order has no expiration date, it lasts until the end of the trading day. 3096 :return: JSON with response from broker server. 3097 """ 3098 if self.accountId is None or not self.accountId: 3099 uLogger.error("Variable `accountId` must be defined for using this method!") 3100 raise Exception("Account ID required") 3101 3102 if operation is None or not operation or operation not in ("Buy", "Sell"): 3103 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3104 raise Exception("Incorrect value") 3105 3106 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3107 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3108 raise Exception("Incorrect value") 3109 3110 if lots is None or lots < 1: 3111 uLogger.error("You must define trade volume > 0: integer count of lots!") 3112 raise Exception("Incorrect value") 3113 3114 if targetPrice is None or targetPrice <= 0: 3115 uLogger.error("Target price for limit-order must be greater than 0!") 3116 raise Exception("Incorrect value") 3117 3118 if limitPrice is None or limitPrice <= 0: 3119 limitPrice = targetPrice 3120 3121 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3122 stopType = "Limit" 3123 3124 if expDate is None or not expDate: 3125 expDate = "Undefined" 3126 3127 if not (self._ticker or self._figi): 3128 uLogger.error("Tocker or FIGI must be defined!") 3129 raise Exception("Ticker or FIGI required") 3130 3131 response = {} 3132 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3133 self._ticker = instrument["ticker"] 3134 self._figi = instrument["figi"] 3135 3136 if orderType == "Limit": 3137 uLogger.debug( 3138 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3139 self._ticker, self._figi, 3140 operation, lots, targetPrice, instrument["currency"], 3141 )) 3142 3143 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3144 self.body = str({ 3145 "figi": self._figi, 3146 "quantity": str(lots), 3147 "price": FloatToNano(targetPrice), 3148 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3149 "accountId": str(self.accountId), 3150 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3151 }) 3152 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3153 3154 if "orderId" in response.keys(): 3155 uLogger.info( 3156 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3157 response["orderId"], 3158 self._ticker, self._figi, 3159 operation, lots, targetPrice, instrument["currency"], 3160 )) 3161 3162 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3163 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3164 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3165 targetPrice, instrument["currency"], 3166 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3167 )) 3168 3169 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3170 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3171 targetPrice, instrument["currency"], 3172 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3173 )) 3174 3175 else: 3176 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3177 3178 if orderType == "Stop": 3179 uLogger.debug( 3180 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3181 self._ticker, self._figi, 3182 operation, lots, 3183 targetPrice, instrument["currency"], 3184 limitPrice, instrument["currency"], 3185 stopType, expDate, 3186 )) 3187 3188 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3189 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3190 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3191 3192 body = { 3193 "figi": self._figi, 3194 "quantity": str(lots), 3195 "price": FloatToNano(limitPrice), 3196 "stopPrice": FloatToNano(targetPrice), 3197 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3198 "accountId": str(self.accountId), 3199 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3200 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3201 } 3202 3203 if expDateUTC: 3204 body["expireDate"] = expDateUTC 3205 3206 self.body = str(body) 3207 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3208 3209 if "stopOrderId" in response.keys(): 3210 uLogger.info( 3211 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3212 response["stopOrderId"], 3213 self._ticker, self._figi, 3214 operation, lots, 3215 targetPrice, instrument["currency"], 3216 limitPrice, instrument["currency"], 3217 TKS_STOP_ORDER_TYPES[stopOrderType], 3218 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3219 )) 3220 3221 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3222 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3223 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3224 targetPrice, instrument["currency"], 3225 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3226 )) 3227 3228 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3229 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3230 targetPrice, instrument["currency"], 3231 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3232 )) 3233 3234 else: 3235 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3236 3237 return response 3238 3239 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3240 """ 3241 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3242 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3243 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3244 See also: `Order()` docstring. 3245 3246 :param lots: volume, integer count of lots >= 1. 3247 :param targetPrice: target price > 0. This is open trade price for limit order. 3248 :return: JSON with response from broker server. 3249 """ 3250 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3251 3252 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3253 """ 3254 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3255 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3256 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3257 target price value then broker opens a limit order. See also: `Order()` docstring. 3258 3259 :param lots: volume, integer count of lots >= 1. 3260 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3261 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3262 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3263 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3264 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3265 :param expDate: string "Undefined" by default or local date in future. 3266 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3267 This date is converting to UTC format for server. 3268 :return: JSON with response from broker server. 3269 """ 3270 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3271 3272 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3273 """ 3274 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3275 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3276 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3277 See also: `Order()` docstring. 3278 3279 :param lots: volume, integer count of lots >= 1. 3280 :param targetPrice: target price > 0. This is open trade price for limit order. 3281 :return: JSON with response from broker server. 3282 """ 3283 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3284 3285 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3286 """ 3287 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3288 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3289 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3290 target price value then broker opens a limit order. See also: `Order()` docstring. 3291 3292 :param lots: volume, integer count of lots >= 1. 3293 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3294 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3295 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3296 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3297 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3298 :param expDate: string "Undefined" by default or local date in future. 3299 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3300 This date is converting to UTC format for server. 3301 :return: JSON with response from broker server. 3302 """ 3303 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3304 3305 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3306 """ 3307 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3308 3309 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3310 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3311 This avoids unnecessary downloading data from the server. 3312 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3313 """ 3314 if self.accountId is None or not self.accountId: 3315 uLogger.error("Variable `accountId` must be defined for using this method!") 3316 raise Exception("Account ID required") 3317 3318 if orderIDs: 3319 if allOrdersIDs is None: 3320 rawOrders = self.RequestPendingOrders() 3321 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3322 3323 if allStopOrdersIDs is None: 3324 rawStopOrders = self.RequestStopOrders() 3325 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3326 3327 for orderID in orderIDs: 3328 idInPendingOrders = orderID in allOrdersIDs 3329 idInStopOrders = orderID in allStopOrdersIDs 3330 3331 if not (idInPendingOrders or idInStopOrders): 3332 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3333 continue 3334 3335 else: 3336 if idInPendingOrders: 3337 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3338 3339 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3340 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3341 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3342 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3343 3344 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3345 if self.moreDebug: 3346 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3347 3348 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3349 3350 else: 3351 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3352 3353 elif idInStopOrders: 3354 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3355 3356 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3357 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3358 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3359 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3360 3361 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3362 if self.moreDebug: 3363 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3364 3365 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3366 3367 else: 3368 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3369 3370 else: 3371 continue 3372 3373 def CloseAllOrders(self) -> None: 3374 """ 3375 Gets a list of open pending and stop orders and cancel it all. 3376 """ 3377 rawOrders = self.RequestPendingOrders() 3378 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3379 lenOrders = len(allOrdersIDs) 3380 3381 rawStopOrders = self.RequestStopOrders() 3382 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3383 lenSOrders = len(allStopOrdersIDs) 3384 3385 if lenOrders > 0 or lenSOrders > 0: 3386 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3387 3388 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3389 3390 else: 3391 uLogger.info("Orders not found, nothing to cancel.") 3392 3393 def CloseAll(self, *args) -> None: 3394 """ 3395 Close all available (not blocked) opened trades and orders. 3396 3397 Also, you can select one or more keywords case-insensitive: 3398 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3399 3400 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3401 """ 3402 overview = self.Overview(show=False) # get all open trades info 3403 3404 if len(args) == 0: 3405 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3406 self.CloseAllOrders() # close all pending and stop orders 3407 3408 for iType in TKS_INSTRUMENTS: 3409 if iType != "Currencies": 3410 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3411 3412 else: 3413 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3414 lowerArgs = [x.lower() for x in args] 3415 3416 if "orders" in lowerArgs: 3417 self.CloseAllOrders() # close all pending and stop orders 3418 3419 for iType in TKS_INSTRUMENTS: 3420 if iType.lower() in lowerArgs and iType != "Currencies": 3421 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3422 3423 def CloseAllByTicker(self, instrument: str) -> None: 3424 """ 3425 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3426 3427 This method searches opened trade and orders of instrument throw all portfolio and then use 3428 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3429 3430 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3431 3432 :param instrument: string with ticker. 3433 """ 3434 if instrument is None or not instrument: 3435 uLogger.error("Ticker name must be defined for using this method!") 3436 raise Exception("Ticker required") 3437 3438 overview = self.Overview(show=False) # get user portfolio with all open trades info 3439 3440 self._ticker = instrument # try to set instrument as ticker 3441 self._figi = "" 3442 3443 if self.IsInPortfolio(portfolio=overview): 3444 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3445 self.CloseTrades(instruments=[instrument], portfolio=overview) 3446 3447 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3448 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3449 3450 if limitAll and self.IsInLimitOrders(portfolio=overview): 3451 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3452 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3453 3454 if stopAll and self.IsInStopOrders(portfolio=overview): 3455 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3456 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3457 3458 def CloseAllByFIGI(self, instrument: str) -> None: 3459 """ 3460 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3461 3462 This method searches opened trade and orders of instrument throw all portfolio and then use 3463 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3464 3465 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3466 3467 :param instrument: string with FIGI id. 3468 """ 3469 if instrument is None or not instrument: 3470 uLogger.error("FIGI id must be defined for using this method!") 3471 raise Exception("FIGI required") 3472 3473 overview = self.Overview(show=False) # get user portfolio with all open trades info 3474 3475 self._ticker = "" 3476 self._figi = instrument # try to set instrument as FIGI id 3477 3478 if self.IsInPortfolio(portfolio=overview): 3479 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3480 self.CloseTrades(instruments=[instrument], portfolio=overview) 3481 3482 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3483 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3484 3485 if limitAll and self.IsInLimitOrders(portfolio=overview): 3486 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3487 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3488 3489 if stopAll and self.IsInStopOrders(portfolio=overview): 3490 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3491 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3492 3493 @staticmethod 3494 def ParseOrderParameters(operation, **inputParameters): 3495 """ 3496 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3497 3498 :param operation: string "Buy" or "Sell". 3499 :param inputParameters: this is dict of strings that looks like this 3500 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3501 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3502 "prices" key: one or more prices to open limit-orders 3503 Counts of values in lots and prices lists must be equals! 3504 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3505 """ 3506 # TODO: update order grid work with api v2 3507 pass 3508 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3509 # 3510 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3511 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3512 # raise Exception("Incorrect value") 3513 # 3514 # if "l" in inputParameters.keys(): 3515 # inputParameters["lots"] = inputParameters.pop("l") 3516 # 3517 # if "p" in inputParameters.keys(): 3518 # inputParameters["prices"] = inputParameters.pop("p") 3519 # 3520 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3521 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3522 # raise Exception("Incorrect value") 3523 # 3524 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3525 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3526 # 3527 # if len(lots) != len(prices): 3528 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3529 # raise Exception("Incorrect value") 3530 # 3531 # uLogger.debug("Extracted parameters for orders:") 3532 # uLogger.debug("lots = {}".format(lots)) 3533 # uLogger.debug("prices = {}".format(prices)) 3534 # 3535 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3536 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3537 # uLogger.debug("Order parameters: {}".format(result)) 3538 # 3539 # return result 3540 3541 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3542 """ 3543 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3544 3545 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3546 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3547 """ 3548 result = False 3549 msg = "Instrument not defined!" 3550 3551 if portfolio is None or not portfolio: 3552 portfolio = self.Overview(show=False) 3553 3554 if self._ticker: 3555 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3556 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3557 3558 for iType in TKS_INSTRUMENTS: 3559 for instrument in portfolio["stat"][iType]: 3560 if instrument["ticker"] == self._ticker: 3561 result = True 3562 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3563 break 3564 3565 elif self._figi: 3566 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3567 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3568 3569 for iType in TKS_INSTRUMENTS: 3570 for instrument in portfolio["stat"][iType]: 3571 if instrument["figi"] == self._figi: 3572 result = True 3573 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3574 break 3575 3576 else: 3577 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3578 3579 uLogger.debug(msg) 3580 3581 return result 3582 3583 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3584 """ 3585 Returns instrument from the user's portfolio if it presents there. 3586 Instrument must be defined by `ticker` (highly priority) or `figi`. 3587 3588 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3589 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3590 """ 3591 result = None 3592 msg = "Instrument not defined!" 3593 3594 if portfolio is None or not portfolio: 3595 portfolio = self.Overview(show=False) 3596 3597 if self._ticker: 3598 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3599 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3600 3601 for iType in TKS_INSTRUMENTS: 3602 for instrument in portfolio["stat"][iType]: 3603 if instrument["ticker"] == self._ticker: 3604 result = instrument 3605 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3606 break 3607 3608 elif self._figi: 3609 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3610 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3611 3612 for iType in TKS_INSTRUMENTS: 3613 for instrument in portfolio["stat"][iType]: 3614 if instrument["figi"] == self._figi: 3615 result = instrument 3616 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3617 break 3618 3619 else: 3620 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3621 3622 uLogger.debug(msg) 3623 3624 return result 3625 3626 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3627 """ 3628 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3629 3630 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3631 3632 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3633 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3634 """ 3635 result = False 3636 msg = "Instrument not defined!" 3637 3638 if portfolio is None or not portfolio: 3639 portfolio = self.Overview(show=False) 3640 3641 if self._ticker: 3642 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3643 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3644 3645 for instrument in portfolio["stat"]["orders"]: 3646 if instrument["ticker"] == self._ticker: 3647 result = True 3648 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3649 break 3650 3651 elif self._figi: 3652 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3653 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3654 3655 for instrument in portfolio["stat"]["orders"]: 3656 if instrument["figi"] == self._figi: 3657 result = True 3658 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3659 break 3660 3661 else: 3662 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3663 3664 uLogger.debug(msg) 3665 3666 return result 3667 3668 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3669 """ 3670 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3671 Instrument must be defined by `ticker` (highly priority) or `figi`. 3672 3673 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3674 3675 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3676 :return: list with `orderID`s of limit orders. 3677 """ 3678 result = [] 3679 msg = "Instrument not defined!" 3680 3681 if portfolio is None or not portfolio: 3682 portfolio = self.Overview(show=False) 3683 3684 if self._ticker: 3685 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3686 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3687 3688 for instrument in portfolio["stat"]["orders"]: 3689 if instrument["ticker"] == self._ticker: 3690 result.append(instrument["orderID"]) 3691 3692 if result: 3693 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3694 3695 elif self._figi: 3696 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3697 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3698 3699 for instrument in portfolio["stat"]["orders"]: 3700 if instrument["figi"] == self._figi: 3701 result.append(instrument["orderID"]) 3702 3703 if result: 3704 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3705 3706 else: 3707 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3708 3709 uLogger.debug(msg) 3710 3711 return result 3712 3713 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3714 """ 3715 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3716 3717 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3718 3719 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3720 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3721 """ 3722 result = False 3723 msg = "Instrument not defined!" 3724 3725 if portfolio is None or not portfolio: 3726 portfolio = self.Overview(show=False) 3727 3728 if self._ticker: 3729 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3730 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3731 3732 for instrument in portfolio["stat"]["stopOrders"]: 3733 if instrument["ticker"] == self._ticker: 3734 result = True 3735 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3736 break 3737 3738 elif self._figi: 3739 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3740 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3741 3742 for instrument in portfolio["stat"]["stopOrders"]: 3743 if instrument["figi"] == self._figi: 3744 result = True 3745 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3746 break 3747 3748 else: 3749 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3750 3751 uLogger.debug(msg) 3752 3753 return result 3754 3755 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3756 """ 3757 Returns list with all `orderID`s of opened stop orders for the instrument. 3758 Instrument must be defined by `ticker` (highly priority) or `figi`. 3759 3760 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3761 3762 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3763 :return: list with `orderID`s of stop orders. 3764 """ 3765 result = [] 3766 msg = "Instrument not defined!" 3767 3768 if portfolio is None or not portfolio: 3769 portfolio = self.Overview(show=False) 3770 3771 if self._ticker: 3772 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3773 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3774 3775 for instrument in portfolio["stat"]["stopOrders"]: 3776 if instrument["ticker"] == self._ticker: 3777 result.append(instrument["orderID"]) 3778 3779 if result: 3780 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3781 3782 elif self._figi: 3783 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3784 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3785 3786 for instrument in portfolio["stat"]["stopOrders"]: 3787 if instrument["figi"] == self._figi: 3788 result.append(instrument["orderID"]) 3789 3790 if result: 3791 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3792 3793 else: 3794 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3795 3796 uLogger.debug(msg) 3797 3798 return result 3799 3800 def RequestLimits(self) -> dict: 3801 """ 3802 Method for obtaining the available funds for withdrawal for current `accountId`. 3803 3804 See also: 3805 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3806 - `OverviewLimits()` method 3807 3808 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3809 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3810 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3811 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3812 """ 3813 if self.accountId is None or not self.accountId: 3814 uLogger.error("Variable `accountId` must be defined for using this method!") 3815 raise Exception("Account ID required") 3816 3817 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3818 3819 self.body = str({"accountId": self.accountId}) 3820 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3821 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3822 3823 if self.moreDebug: 3824 uLogger.debug("Records about available funds for withdrawal successfully received") 3825 3826 return rawLimits 3827 3828 def OverviewLimits(self, show: bool = False) -> dict: 3829 """ 3830 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3831 3832 See also: `RequestLimits()`. 3833 3834 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3835 :return: dict with raw parsed data from server and some calculated statistics about it. 3836 """ 3837 if self.accountId is None or not self.accountId: 3838 uLogger.error("Variable `accountId` must be defined for using this method!") 3839 raise Exception("Account ID required") 3840 3841 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3842 3843 view = { 3844 "rawLimits": rawLimits, 3845 "limits": { # parsed data for every currency: 3846 "money": { # this is an array of portfolio currency positions 3847 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3848 }, 3849 "blocked": { # this is an array of blocked currency 3850 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3851 }, 3852 "blockedGuarantee": { # this is locked money under collateral for futures 3853 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3854 }, 3855 }, 3856 } 3857 3858 # --- Prepare text table with limits in human-readable format: 3859 if show: 3860 info = [ 3861 "# Withdrawal limits\n\n", 3862 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3863 "* **Account ID:** [{}]\n".format(self.accountId), 3864 ] 3865 3866 if view["limits"]["money"]: 3867 info.extend([ 3868 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3869 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3870 ]) 3871 3872 else: 3873 info.append("\nNo withdrawal limits\n") 3874 3875 for curr in view["limits"]["money"].keys(): 3876 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3877 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3878 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3879 3880 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3881 "[{}]".format(curr), 3882 "{:.2f}".format(view["limits"]["money"][curr]), 3883 "{:.2f}".format(availableMoney), 3884 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3885 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3886 ) 3887 3888 if curr == "rub": 3889 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3890 3891 else: 3892 info.append(infoStr) 3893 3894 infoText = "".join(info) 3895 3896 uLogger.info(infoText) 3897 3898 if self.withdrawalLimitsFile: 3899 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3900 fH.write(infoText) 3901 3902 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3903 3904 return view 3905 3906 def RequestAccounts(self) -> dict: 3907 """ 3908 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3909 3910 See also: 3911 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3912 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3913 - `OverviewUserInfo()` method 3914 3915 :return: dict with raw data from server that contains accounts info. Example of dict: 3916 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3917 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3918 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3919 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3920 """ 3921 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3922 3923 self.body = str({}) 3924 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3925 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3926 3927 if self.moreDebug: 3928 uLogger.debug("Records about available accounts successfully received") 3929 3930 return rawAccounts 3931 3932 def RequestUserInfo(self) -> dict: 3933 """ 3934 Method for requesting common user's information. 3935 3936 See also: 3937 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3938 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3939 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3940 - `OverviewUserInfo()` method 3941 3942 :return: dict with raw data from server that contains user's information. Example of dict: 3943 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3944 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3945 """ 3946 uLogger.debug("Requesting common user's information. Wait, please...") 3947 3948 self.body = str({}) 3949 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3950 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3951 3952 if self.moreDebug: 3953 uLogger.debug("Records about current user successfully received") 3954 3955 return rawUserInfo 3956 3957 def RequestMarginStatus(self, accountId: str = None) -> dict: 3958 """ 3959 Method for requesting margin calculation for defined account ID. 3960 3961 See also: 3962 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3963 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3964 - `OverviewUserInfo()` method 3965 3966 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3967 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3968 Example of responses: 3969 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3970 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3971 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3972 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3973 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3974 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3975 """ 3976 if accountId is None or not accountId: 3977 if self.accountId is None or not self.accountId: 3978 uLogger.error("Variable `accountId` must be defined for using this method!") 3979 raise Exception("Account ID required") 3980 3981 else: 3982 accountId = self.accountId # use `self.accountId` (main ID) by default 3983 3984 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3985 3986 self.body = str({"accountId": accountId}) 3987 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3988 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3989 3990 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3991 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3992 rawMargin = {} 3993 3994 else: 3995 if self.moreDebug: 3996 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3997 3998 return rawMargin 3999 4000 def RequestTariffLimits(self) -> dict: 4001 """ 4002 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4003 4004 See also: 4005 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4006 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4007 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4008 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4009 - `OverviewUserInfo()` method 4010 4011 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4012 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4013 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4014 """ 4015 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4016 4017 self.body = str({}) 4018 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4019 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4020 4021 if self.moreDebug: 4022 uLogger.debug("Records with limits of current tariff successfully received") 4023 4024 return rawTariffLimits 4025 4026 def RequestBondCoupons(self, iJSON: dict) -> dict: 4027 """ 4028 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4029 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4030 All dates are in UTC timezone. 4031 4032 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4033 Documentation: 4034 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4035 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4036 4037 See also: `ExtendBondsData()`. 4038 4039 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4040 If raw iJSON is not data of bond then server returns an error [400] with message: 4041 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4042 :return: dictionary with bond payment calendar. Response example 4043 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4044 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4045 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4046 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4047 """ 4048 if iJSON["figi"] is None or not iJSON["figi"]: 4049 uLogger.error("FIGI must be defined for using this method!") 4050 raise Exception("FIGI required") 4051 4052 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4053 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4054 4055 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4056 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4057 self._figi, 4058 startDate, 4059 endDate, 4060 )) 4061 4062 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4063 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4064 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4065 4066 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4067 uLogger.warning("Instrument type is not bond!") 4068 4069 else: 4070 if self.moreDebug: 4071 uLogger.debug("Records about bond payment calendar successfully received") 4072 4073 return calendar 4074 4075 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4076 """ 4077 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4078 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4079 coupon yields, current yields and some statistics etc. 4080 4081 WARNING! This is too long operation if a lot of bonds requested from broker server. 4082 4083 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4084 4085 :param instruments: list of strings with tickers or FIGIs. 4086 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4087 for further used by data scientists or stock analytics. 4088 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4089 In XLSX-file and Pandas DataFrame fields mean: 4090 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4091 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4092 """ 4093 if instruments is None or not instruments: 4094 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4095 raise Exception("Ticker or FIGI required") 4096 4097 if isinstance(instruments, str): 4098 instruments = [instruments] 4099 4100 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4101 4102 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4103 4104 iCount = len(uniqueInstruments) 4105 tooLong = iCount >= 20 4106 if tooLong: 4107 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4108 4109 bonds = None 4110 for i, self._figi in enumerate(uniqueInstruments): 4111 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4112 4113 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4114 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4115 rawBond = self.SearchByFIGI(requestPrice=True) 4116 4117 # Widen raw data with UTC current time (iData["actualDateTime"]): 4118 actualDate = datetime.now(tzutc()) 4119 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4120 4121 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4122 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4123 4124 # Replace some values with human-readable: 4125 iData["nominalCurrency"] = iData["nominal"]["currency"] 4126 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4127 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4128 iData["aciCurrency"] = iData["aciValue"]["currency"] 4129 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4130 iData["issueSize"] = int(iData["issueSize"]) 4131 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4132 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4133 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4134 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4135 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4136 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4137 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4138 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4139 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4140 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4141 4142 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4143 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4144 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4145 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4146 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4147 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4148 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4149 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4150 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4151 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4152 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4153 4154 # Widen raw data with calendar data from `rawCalendar` values: 4155 calendarData = [] 4156 if "events" in iData["rawCalendar"].keys(): 4157 for item in iData["rawCalendar"]["events"]: 4158 calendarData.append({ 4159 "couponDate": item["couponDate"], 4160 "couponNumber": int(item["couponNumber"]), 4161 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4162 "payCurrency": item["payOneBond"]["currency"], 4163 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4164 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4165 "couponStartDate": item["couponStartDate"], 4166 "couponEndDate": item["couponEndDate"], 4167 "couponPeriod": item["couponPeriod"], 4168 }) 4169 4170 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4171 if "maturityDate" not in iData.keys(): 4172 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4173 4174 # Widen raw data with Coupon Rate. 4175 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4176 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4177 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4178 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4179 4180 # Widen raw data with Yield to Maturity (YTM) on current date. 4181 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4182 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4183 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4184 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4185 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4186 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4187 4188 iData["calendar"] = calendarData # adds calendar at the end 4189 4190 # Remove not used data: 4191 iData.pop("uid") 4192 iData.pop("positionUid") 4193 iData.pop("currentPrice") 4194 iData.pop("rawCalendar") 4195 4196 colNames = list(iData.keys()) 4197 if bonds is None: 4198 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4199 4200 else: 4201 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4202 4203 else: 4204 uLogger.warning("Instrument is not a bond!") 4205 4206 processed = round(100 * (i + 1) / iCount, 1) 4207 if tooLong and processed % 5 == 0: 4208 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4209 4210 else: 4211 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4212 4213 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4214 4215 # Saving bonds from Pandas DataFrame to XLSX sheet: 4216 if xlsx and self.bondsXLSXFile: 4217 with pd.ExcelWriter( 4218 path=self.bondsXLSXFile, 4219 date_format=TKS_DATE_FORMAT, 4220 datetime_format=TKS_DATE_TIME_FORMAT, 4221 mode="w", 4222 ) as writer: 4223 bonds.to_excel( 4224 writer, 4225 sheet_name="Extended bonds data", 4226 index=True, 4227 encoding="UTF-8", 4228 freeze_panes=(1, 1), 4229 ) # saving as XLSX-file with freeze first row and column as headers 4230 4231 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4232 4233 return bonds 4234 4235 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4236 """ 4237 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4238 4239 WARNING! This is too long operation if a lot of bonds requested from broker server. 4240 4241 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4242 4243 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4244 extended information about bonds: main info, current prices, bond payment calendar, 4245 coupon yields, current yields and some statistics etc. 4246 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4247 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4248 for further used by data scientists or stock analytics. 4249 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4250 """ 4251 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4252 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4253 4254 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4255 4256 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4257 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4258 calendar = None 4259 for bond in extBonds.iterrows(): 4260 for item in bond[1]["calendar"]: 4261 cData = { 4262 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4263 "couponDate": item["couponDate"], 4264 "figi": bond[1]["figi"], 4265 "ticker": bond[1]["ticker"], 4266 "name": bond[1]["name"], 4267 "couponNumber": item["couponNumber"], 4268 "payOneBond": item["payOneBond"], 4269 "payCurrency": item["payCurrency"], 4270 "couponType": item["couponType"], 4271 "couponPeriod": item["couponPeriod"], 4272 "fixDate": item["fixDate"], 4273 "couponStartDate": item["couponStartDate"], 4274 "couponEndDate": item["couponEndDate"], 4275 } 4276 4277 if calendar is None: 4278 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4279 4280 else: 4281 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4282 4283 if calendar is not None: 4284 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4285 4286 # Saving calendar from Pandas DataFrame to XLSX sheet: 4287 if xlsx: 4288 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4289 4290 with pd.ExcelWriter( 4291 path=xlsxCalendarFile, 4292 date_format=TKS_DATE_FORMAT, 4293 datetime_format=TKS_DATE_TIME_FORMAT, 4294 mode="w", 4295 ) as writer: 4296 humanReadable = calendar.copy(deep=True) 4297 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4298 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4299 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4300 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4301 humanReadable.columns = colNames # human-readable column names 4302 4303 humanReadable.to_excel( 4304 writer, 4305 sheet_name="Bond payments calendar", 4306 index=False, 4307 encoding="UTF-8", 4308 freeze_panes=(1, 2), 4309 ) # saving as XLSX-file with freeze first row and column as headers 4310 4311 del humanReadable # release df in memory 4312 4313 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4314 4315 return calendar 4316 4317 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4318 """ 4319 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4320 Also, creates Markdown file with calendar data, `calendar.md` by default. 4321 4322 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4323 4324 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4325 extended information about bonds: main info, current prices, bond payment calendar, 4326 coupon yields, current yields and some statistics etc. 4327 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4328 :param show: if `True` then also printing bonds payment calendar to the console, 4329 otherwise save to file `calendarFile` only. `False` by default. 4330 :return: multilines text in Markdown format with bonds payment calendar as a table. 4331 """ 4332 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4333 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4334 4335 infoText = "# Bond payments calendar\n\n" 4336 4337 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4338 4339 if not (calendar is None or calendar.empty): 4340 splitLine = "| | | | | | | | | |\n" 4341 4342 info = [ 4343 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4344 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4345 ] 4346 4347 newMonth = False 4348 notOneBond = calendar["figi"].nunique() > 1 4349 for i, bond in enumerate(calendar.iterrows()): 4350 if newMonth and notOneBond: 4351 info.append(splitLine) 4352 4353 info.append( 4354 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4355 " √" if bond[1]["paid"] else " —", 4356 bond[1]["couponDate"].split("T")[0], 4357 bond[1]["figi"], 4358 bond[1]["ticker"], 4359 bond[1]["couponNumber"], 4360 "{} {}".format( 4361 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4362 bond[1]["payCurrency"], 4363 ), 4364 bond[1]["couponType"], 4365 bond[1]["couponPeriod"], 4366 bond[1]["fixDate"].split("T")[0], 4367 ) 4368 ) 4369 4370 if i < len(calendar.values) - 1: 4371 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4372 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4373 newMonth = False if curDate.month == nextDate.month else True 4374 4375 else: 4376 newMonth = False 4377 4378 infoText += "".join(info) 4379 4380 if show: 4381 uLogger.info("{}".format(infoText)) 4382 4383 if self.calendarFile is not None: 4384 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4385 fH.write(infoText) 4386 4387 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4388 4389 else: 4390 infoText += "No data\n" 4391 4392 return infoText 4393 4394 def OverviewAccounts(self, show: bool = False) -> dict: 4395 """ 4396 Method for parsing and show simple table with all available user accounts. 4397 4398 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4399 4400 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4401 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4402 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4403 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4404 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4405 "closed": "—", "access": "Full access" }, ...}}` 4406 """ 4407 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4408 4409 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4410 accounts = { 4411 item["id"]: { 4412 "type": TKS_ACCOUNT_TYPES[item["type"]], 4413 "name": item["name"], 4414 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4415 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4416 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4417 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4418 } for item in rawAccounts["accounts"] 4419 } 4420 4421 # Raw and parsed data with some fields replaced in "stat" section: 4422 view = { 4423 "rawAccounts": rawAccounts, 4424 "stat": accounts, 4425 } 4426 4427 # --- Prepare simple text table with only accounts data in human-readable format: 4428 if show: 4429 info = [ 4430 "# User accounts\n\n", 4431 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4432 "| Account ID | Type | Status | Name |\n", 4433 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4434 ] 4435 4436 for account in view["stat"].keys(): 4437 info.extend([ 4438 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4439 account, 4440 view["stat"][account]["type"], 4441 view["stat"][account]["status"], 4442 view["stat"][account]["name"], 4443 ) 4444 ]) 4445 4446 infoText = "".join(info) 4447 4448 uLogger.info(infoText) 4449 4450 if self.userAccountsFile: 4451 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4452 fH.write(infoText) 4453 4454 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4455 4456 return view 4457 4458 def OverviewUserInfo(self, show: bool = False) -> dict: 4459 """ 4460 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4461 4462 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4463 4464 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4465 :return: dict with raw parsed data from server and some calculated statistics about it. 4466 """ 4467 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4468 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4469 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4470 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4471 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4472 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4473 4474 # This is dict with parsed common user data: 4475 userInfo = { 4476 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4477 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4478 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4479 "tariff": rawUserInfo["tariff"], 4480 } 4481 4482 # This is an array of dict with parsed margin statuses for every account IDs: 4483 margins = {} 4484 for accountId in accounts.keys(): 4485 if rawMargins[accountId]: 4486 margins[accountId] = { 4487 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4488 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4489 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4490 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4491 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4492 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4493 } 4494 4495 else: 4496 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4497 4498 unary = {} # unary-connection limits 4499 for item in rawTariffLimits["unaryLimits"]: 4500 if item["limitPerMinute"] in unary.keys(): 4501 unary[item["limitPerMinute"]].extend(item["methods"]) 4502 4503 else: 4504 unary[item["limitPerMinute"]] = item["methods"] 4505 4506 stream = {} # stream-connection limits 4507 for item in rawTariffLimits["streamLimits"]: 4508 if item["limit"] in stream.keys(): 4509 stream[item["limit"]].extend(item["streams"]) 4510 4511 else: 4512 stream[item["limit"]] = item["streams"] 4513 4514 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4515 limits = { 4516 "unary": unary, 4517 "stream": stream, 4518 } 4519 4520 # Raw and parsed data as an output result: 4521 view = { 4522 "rawUserInfo": rawUserInfo, 4523 "rawAccounts": rawAccounts, 4524 "rawMargins": rawMargins, 4525 "rawTariffLimits": rawTariffLimits, 4526 "stat": { 4527 "userInfo": userInfo, 4528 "accounts": accounts, 4529 "margins": margins, 4530 "limits": limits, 4531 }, 4532 } 4533 4534 # --- Prepare text table with user information in human-readable format: 4535 if show: 4536 info = [ 4537 "# Full user information\n\n", 4538 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4539 "## Common information\n\n", 4540 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4541 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4542 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4543 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4544 "\n## User accounts\n\n", 4545 ] 4546 4547 for account in view["stat"]["accounts"].keys(): 4548 info.extend([ 4549 "### ID: [{}]\n\n".format(account), 4550 "| Parameters | Values |\n", 4551 "|----------------------|--------------------------------------------------------------|\n", 4552 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4553 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4554 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4555 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4556 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4557 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4558 ]) 4559 4560 if margins[account]: 4561 info.extend([ 4562 "| Margin status: | Enabled |\n", 4563 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4564 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4565 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4566 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4567 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4568 ]) 4569 4570 else: 4571 info.append("| Margin status: | Disabled |\n\n") 4572 4573 info.extend([ 4574 "\n## Current user tariff limits\n", 4575 "\nSee also:\n", 4576 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4577 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4578 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4579 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4580 "\n### Unary limits\n", 4581 ]) 4582 4583 if unary: 4584 for key, values in sorted(unary.items()): 4585 info.append("\n* Max requests per minute: {}\n".format(key)) 4586 4587 for value in values: 4588 info.append(" - {}\n".format(value)) 4589 4590 else: 4591 info.append("\nNot available\n") 4592 4593 info.append("\n### Stream limits\n") 4594 4595 if stream: 4596 for key, values in sorted(stream.items()): 4597 info.append("\n* Max stream connections: {}\n".format(key)) 4598 4599 for value in values: 4600 info.append(" - {}\n".format(value)) 4601 4602 else: 4603 info.append("\nNot available\n") 4604 4605 infoText = "".join(info) 4606 4607 uLogger.info(infoText) 4608 4609 if self.userInfoFile: 4610 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4611 fH.write(infoText) 4612 4613 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4614 4615 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self._ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self._figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
402 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 403 """ 404 Send GET or POST request to broker server and receive JSON object. 405 406 self.header: must be defining with dictionary of headers. 407 self.body: if define then used as request body. None by default. 408 self.timeout: global request timeout, 15 seconds by default. 409 :param url: url with REST request. 410 :param reqType: send "GET" or "POST" request. "GET" by default. 411 :param retry: how many times retry after first request if an 5xx server errors occurred. 412 :param pause: sleep time in seconds between retries. 413 :return: response JSON (dictionary) from broker. 414 """ 415 if reqType.upper() not in ("GET", "POST"): 416 uLogger.error("You can define request type: `GET` or `POST`!") 417 raise Exception("Incorrect value") 418 419 if self.moreDebug: 420 uLogger.debug("Request parameters:") 421 uLogger.debug(" - REST API URL: {}".format(url)) 422 uLogger.debug(" - request type: {}".format(reqType)) 423 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 424 uLogger.debug(" - body:\n{}".format(self.body)) 425 426 # fast hack to avoid all operations with some tickers/FIGI 427 responseJSON = {} 428 oK = True 429 for item in self.exclude: 430 if item in url: 431 if self.moreDebug: 432 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 433 434 oK = False 435 break 436 437 if oK: 438 counter = 0 439 response = None 440 errMsg = "" 441 442 while not response and counter <= retry: 443 if reqType == "GET": 444 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 445 446 if reqType == "POST": 447 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 448 449 if self.moreDebug: 450 uLogger.debug("Response:") 451 uLogger.debug(" - status code: {}".format(response.status_code)) 452 uLogger.debug(" - reason: {}".format(response.reason)) 453 uLogger.debug(" - body length: {}".format(len(response.text))) 454 uLogger.debug(" - headers:\n{}".format(response.headers)) 455 456 # Server returns some headers: 457 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 458 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 459 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 460 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 461 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 462 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 463 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 464 sleep(rateLimitWait) 465 466 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 467 if 400 <= response.status_code < 500: 468 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 469 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 470 471 if "code" in response.text and "message" in response.text: 472 msgDict = self._ParseJSON(rawData=response.text) 473 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 474 475 counter = retry + 1 # do not retry for 4xx errors 476 477 if 500 <= response.status_code < 600: 478 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 479 uLogger.debug(" - not oK, {}".format(errMsg)) 480 481 if "code" in response.text and "message" in response.text: 482 errMsgDict = self._ParseJSON(rawData=response.text) 483 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 484 485 counter += 1 486 487 if counter <= retry: 488 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 489 sleep(pause) 490 491 responseJSON = self._ParseJSON(rawData=response.text) 492 493 if errMsg: 494 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 495 uLogger.error(" - not oK, {}".format(errMsg)) 496 497 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
530 def Listing(self) -> dict: 531 """ 532 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 533 534 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 535 """ 536 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 537 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 538 539 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 540 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 541 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 542 543 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 544 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 545 poolUpdater.close() 546 547 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 548 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 549 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 550 551 # calculate minimum price increment (step) for all instruments and set up instrument's type: 552 for iType in iList.keys(): 553 for ticker in iList[iType]: 554 iList[iType][ticker]["type"] = iType 555 556 if "minPriceIncrement" in iList[iType][ticker].keys(): 557 iList[iType][ticker]["step"] = NanoToFloat( 558 iList[iType][ticker]["minPriceIncrement"]["units"], 559 iList[iType][ticker]["minPriceIncrement"]["nano"], 560 ) 561 562 else: 563 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 564 565 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
567 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 568 """ 569 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 570 571 See also: `DumpInstruments()`, `Listing()`. 572 573 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 574 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 575 """ 576 if self.iListDumpFile is None or not self.iListDumpFile: 577 uLogger.error("Output name of dump file must be defined!") 578 raise Exception("Filename required") 579 580 if not self.iList or forceUpdate: 581 self.iList = self.Listing() 582 583 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 584 585 # Save as XLSX with separated sheets for every type of instruments: 586 with pd.ExcelWriter( 587 path=xlsxDumpFile, 588 date_format=TKS_DATE_FORMAT, 589 datetime_format=TKS_DATE_TIME_FORMAT, 590 mode="w", 591 ) as writer: 592 for iType in TKS_INSTRUMENTS: 593 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 594 df = df[sorted(df)] # sorted by column names 595 df = df.applymap( 596 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 597 na_action="ignore", 598 ) # converting numbers from nano-type to float in every cell 599 df.to_excel( 600 writer, 601 sheet_name=iType, 602 encoding="UTF-8", 603 freeze_panes=(1, 1), 604 ) # saving as XLSX-file with freeze first row and column as headers 605 606 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
608 def DumpInstruments(self, forceUpdate: bool = True) -> str: 609 """ 610 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 611 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 612 613 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 614 615 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 616 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 617 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 618 """ 619 if self.iListDumpFile is None or not self.iListDumpFile: 620 uLogger.error("Output name of dump file must be defined!") 621 raise Exception("Filename required") 622 623 if not self.iList or forceUpdate: 624 self.iList = self.Listing() 625 626 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 627 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 628 fH.write(jsonDump) 629 630 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 631 632 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
634 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 635 """ 636 Show information about one instrument defined by json data and prints it in Markdown format. 637 638 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 639 640 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 641 :param show: if `True` then also printing information about instrument and its current price. 642 :return: multilines text in Markdown format with information about one instrument. 643 """ 644 splitLine = "| | |\n" 645 infoText = "" 646 647 if iJSON is not None and iJSON and isinstance(iJSON, dict): 648 info = [ 649 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 650 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 651 "| Parameters | Values |\n", 652 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 653 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 654 "| Full name: | {:<54} |\n".format(iJSON["name"]), 655 ] 656 657 if "sector" in iJSON.keys() and iJSON["sector"]: 658 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 659 660 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 661 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 662 663 info.extend([ 664 splitLine, 665 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 666 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 667 ]) 668 669 if "isin" in iJSON.keys() and iJSON["isin"]: 670 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 671 672 if "classCode" in iJSON.keys(): 673 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 674 675 info.extend([ 676 splitLine, 677 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 678 splitLine, 679 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 680 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 681 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 682 ]) 683 684 if iJSON["figi"]: 685 self._figi = iJSON["figi"] 686 iJSON = iJSON | self.RequestTradingStatus() 687 688 info.extend([ 689 splitLine, 690 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 691 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 692 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 693 ]) 694 695 info.append(splitLine) 696 697 if "type" in iJSON.keys() and iJSON["type"]: 698 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 699 700 if "shareType" in iJSON.keys() and iJSON["shareType"]: 701 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 702 703 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 704 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 705 706 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 707 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 708 709 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 710 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 711 712 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 713 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 714 715 if "focusType" in iJSON.keys() and iJSON["focusType"]: 716 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 717 718 if "assetType" in iJSON.keys() and iJSON["assetType"]: 719 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 720 721 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 722 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 723 724 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 725 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 726 727 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 728 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 729 730 if "currency" in iJSON.keys(): 731 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 732 733 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 734 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 735 736 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 737 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 738 739 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 740 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 741 742 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 743 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 744 745 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 746 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 747 748 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 749 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 750 751 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 752 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 753 754 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 755 info.append("| Perpetual bond: | Yes |\n") 756 757 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 758 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 759 760 iExt = None 761 if iJSON["type"] == "Bonds": 762 info.extend([ 763 splitLine, 764 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 765 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 766 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 767 iJSON["nominal"]["currency"], 768 )), 769 ]) 770 771 if "floatingCouponFlag" in iJSON.keys(): 772 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 773 774 if "amortizationFlag" in iJSON.keys(): 775 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 776 777 info.append(splitLine) 778 779 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 780 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 781 782 if iJSON["figi"]: 783 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 784 785 info.extend([ 786 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 787 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 788 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 789 ]) 790 791 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 792 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 793 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 794 iJSON["aciValue"]["currency"] 795 ))) 796 797 if "currentPrice" in iJSON.keys(): 798 info.append(splitLine) 799 800 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 801 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 802 803 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 804 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 805 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 806 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 807 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 808 809 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 810 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 811 812 info.extend([ 813 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 814 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 815 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 816 )), 817 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 818 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 819 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 820 )), 821 "| Changes between last deal price and last close | {:<54} |\n".format( 822 "{:.2f}%{}".format( 823 iJSON["currentPrice"]["changes"], 824 " ({}{:.2f} {})".format( 825 "+" if bondChangesDelta > 0 else "", 826 bondChangesDelta, 827 aciCurrency 828 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 829 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 830 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 831 currency 832 ), 833 ) 834 ), 835 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 836 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 837 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 838 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 839 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 840 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 841 )), 842 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 843 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 844 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 845 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 846 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 847 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 848 )), 849 ]) 850 851 if "lot" in iJSON.keys(): 852 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 853 854 if "step" in iJSON.keys() and iJSON["step"] != 0: 855 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 856 857 # Add bond payment calendar: 858 if iJSON["type"] == "Bonds": 859 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 860 info.extend(["\n", strCalendar]) 861 862 infoText += "".join(info) 863 864 if show: 865 uLogger.info("{}".format(infoText)) 866 867 else: 868 uLogger.debug("{}".format(infoText)) 869 870 if self.infoFile is not None: 871 with open(self.infoFile, "w", encoding="UTF-8") as fH: 872 fH.write(infoText) 873 874 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 875 876 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
878 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 879 """ 880 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 881 882 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 883 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 884 :return: JSON formatted data with information about instrument. 885 """ 886 tickerJSON = {} 887 if self.moreDebug: 888 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 889 890 if not self._ticker: 891 uLogger.warning("self._ticker variable is not be empty!") 892 893 else: 894 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 895 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 896 raise Exception("Instrument not allowed") 897 898 if not self.iList: 899 self.iList = self.Listing() 900 901 if self._ticker in self.iList["Shares"].keys(): 902 tickerJSON = self.iList["Shares"][self._ticker] 903 if self.moreDebug: 904 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 905 906 elif self._ticker in self.iList["Currencies"].keys(): 907 tickerJSON = self.iList["Currencies"][self._ticker] 908 if self.moreDebug: 909 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 910 911 elif self._ticker in self.iList["Bonds"].keys(): 912 tickerJSON = self.iList["Bonds"][self._ticker] 913 if self.moreDebug: 914 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 915 916 elif self._ticker in self.iList["Etfs"].keys(): 917 tickerJSON = self.iList["Etfs"][self._ticker] 918 if self.moreDebug: 919 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 920 921 elif self._ticker in self.iList["Futures"].keys(): 922 tickerJSON = self.iList["Futures"][self._ticker] 923 if self.moreDebug: 924 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 925 926 if tickerJSON: 927 self._figi = tickerJSON["figi"] 928 929 if requestPrice: 930 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 931 932 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 933 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 934 935 else: 936 tickerJSON["currentPrice"]["changes"] = 0 937 938 if show: 939 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 940 941 else: 942 if show: 943 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 944 945 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
947 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 948 """ 949 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 950 951 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 952 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 953 :return: JSON formatted data with information about instrument. 954 """ 955 figiJSON = {} 956 if self.moreDebug: 957 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 958 959 if not self._figi: 960 uLogger.warning("self._figi variable is not be empty!") 961 962 else: 963 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 964 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 965 raise Exception("Instrument not allowed") 966 967 if not self.iList: 968 self.iList = self.Listing() 969 970 for item in self.iList["Shares"].keys(): 971 if self._figi == self.iList["Shares"][item]["figi"]: 972 figiJSON = self.iList["Shares"][item] 973 974 if self.moreDebug: 975 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 976 977 break 978 979 if not figiJSON: 980 for item in self.iList["Currencies"].keys(): 981 if self._figi == self.iList["Currencies"][item]["figi"]: 982 figiJSON = self.iList["Currencies"][item] 983 984 if self.moreDebug: 985 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 986 987 break 988 989 if not figiJSON: 990 for item in self.iList["Bonds"].keys(): 991 if self._figi == self.iList["Bonds"][item]["figi"]: 992 figiJSON = self.iList["Bonds"][item] 993 994 if self.moreDebug: 995 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 996 997 break 998 999 if not figiJSON: 1000 for item in self.iList["Etfs"].keys(): 1001 if self._figi == self.iList["Etfs"][item]["figi"]: 1002 figiJSON = self.iList["Etfs"][item] 1003 1004 if self.moreDebug: 1005 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1006 1007 break 1008 1009 if not figiJSON: 1010 for item in self.iList["Futures"].keys(): 1011 if self._figi == self.iList["Futures"][item]["figi"]: 1012 figiJSON = self.iList["Futures"][item] 1013 1014 if self.moreDebug: 1015 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1016 1017 break 1018 1019 if figiJSON: 1020 self._figi = figiJSON["figi"] 1021 self._ticker = figiJSON["ticker"] 1022 1023 if requestPrice: 1024 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1025 1026 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1027 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1028 1029 else: 1030 figiJSON["currentPrice"]["changes"] = 0 1031 1032 if show: 1033 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1034 1035 else: 1036 if show: 1037 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1038 1039 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1041 def GetCurrentPrices(self, show: bool = True) -> dict: 1042 """ 1043 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1044 `{"buy": [{"price": 1243.8, "quantity": 193}, 1045 {"price": 1244.0, "quantity": 168}, 1046 {"price": 1244.8, "quantity": 5}, 1047 {"price": 1245.0, "quantity": 61}, 1048 {"price": 1245.4, "quantity": 60}], 1049 "sell": [{"price": 1243.6, "quantity": 8}, 1050 {"price": 1242.6, "quantity": 10}, 1051 {"price": 1242.4, "quantity": 18}, 1052 {"price": 1242.2, "quantity": 50}, 1053 {"price": 1242.0, "quantity": 113}], 1054 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1055 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1056 - sell: list of dicts with Buyers prices, 1057 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1058 - quantity: volume value by current price in lots, 1059 - limitUp: current trade session limit price, maximum, 1060 - limitDown: current trade session limit price, minimum, 1061 - lastPrice: last deal price of the instrument, 1062 - closePrice: previous trade session close price of the instrument. 1063 1064 See also: `SearchByTicker()` and `SearchByFIGI()`. 1065 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1066 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1067 1068 :param show: if `True` then print DOM to log and console. 1069 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1070 If an error occurred then returns an empty record: 1071 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1072 """ 1073 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1074 1075 if self.depth < 1: 1076 uLogger.error("Depth of Market (DOM) must be >=1!") 1077 raise Exception("Incorrect value") 1078 1079 if not (self._ticker or self._figi): 1080 uLogger.error("self._ticker or self._figi variables must be defined!") 1081 raise Exception("Ticker or FIGI required") 1082 1083 if self._ticker and not self._figi: 1084 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1085 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1086 1087 if not self._ticker and self._figi: 1088 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1089 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1090 1091 if not self._figi: 1092 uLogger.error("FIGI is not defined!") 1093 raise Exception("Ticker or FIGI required") 1094 1095 else: 1096 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1097 1098 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1099 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1100 self.body = str({"figi": self._figi, "depth": self.depth}) 1101 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1102 1103 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1104 # list of dicts with sellers orders: 1105 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1106 1107 # list of dicts with buyers orders: 1108 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1109 1110 # max price of instrument at this time: 1111 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1112 1113 # min price of instrument at this time: 1114 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1115 1116 # last price of deal with instrument: 1117 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1118 1119 # last close price of instrument: 1120 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1121 1122 else: 1123 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1124 uLogger.debug("Server response: {}".format(pricesResponse)) 1125 1126 if show: 1127 if prices["buy"] or prices["sell"]: 1128 info = [ 1129 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1130 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1131 self._ticker, 1132 self._figi, 1133 self.depth, 1134 ), 1135 "-" * 60, "\n", 1136 " Orders of Buyers | Orders of Sellers\n", 1137 "-" * 60, "\n", 1138 " Sell prices (volumes) | Buy prices (volumes)\n", 1139 "-" * 60, "\n", 1140 ] 1141 1142 if not prices["buy"]: 1143 info.append(" | No orders!\n") 1144 sumBuy = 0 1145 1146 else: 1147 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1148 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1149 for item in maxMinSorted: 1150 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1151 1152 if not prices["sell"]: 1153 info.append("No orders! |\n") 1154 sumSell = 0 1155 1156 else: 1157 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1158 for item in prices["sell"]: 1159 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1160 1161 info.extend([ 1162 "-" * 60, "\n", 1163 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1164 "-" * 60, "\n", 1165 ]) 1166 1167 infoText = "".join(info) 1168 1169 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1170 1171 else: 1172 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1173 1174 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1176 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1177 """ 1178 This method get and show information about all available broker instruments for current user account. 1179 If `instrumentsFile` string is not empty then also save information to this file. 1180 1181 :param show: if `True` then print results to console, if `False` — print only to file. 1182 :return: multi-lines string with all available broker instruments 1183 """ 1184 if not self.iList: 1185 self.iList = self.Listing() 1186 1187 info = [ 1188 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1189 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1190 ] 1191 1192 # add instruments count by type: 1193 for iType in self.iList.keys(): 1194 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1195 1196 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1197 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1198 1199 # generating info tables with all instruments by type: 1200 for iType in self.iList.keys(): 1201 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1202 1203 for instrument in self.iList[iType].keys(): 1204 iName = self.iList[iType][instrument]["name"] # instrument's name 1205 if len(iName) > 57: 1206 iName = "{}...".format(iName[:54]) # right trim for a long string 1207 1208 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1209 self.iList[iType][instrument]["ticker"], 1210 iName, 1211 self.iList[iType][instrument]["figi"], 1212 self.iList[iType][instrument]["currency"], 1213 self.iList[iType][instrument]["lot"], 1214 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1215 )) 1216 1217 infoText = "".join(info) 1218 1219 if show: 1220 uLogger.info(infoText) 1221 1222 if self.instrumentsFile: 1223 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1224 fH.write(infoText) 1225 1226 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1227 1228 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1230 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1231 """ 1232 This method search and show information about instruments by part of its ticker, FIGI or name. 1233 If `searchResultsFile` string is not empty then also save information to this file. 1234 1235 :param pattern: string with part of ticker, FIGI or instrument's name. 1236 :param show: if `True` then print results to console, if `False` — return list of result only. 1237 :return: list of dictionaries with all found instruments. 1238 """ 1239 if not self.iList: 1240 self.iList = self.Listing() 1241 1242 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1243 compiledPattern = re.compile(pattern, re.IGNORECASE) 1244 1245 for iType in self.iList: 1246 for instrument in self.iList[iType].values(): 1247 searchResult = compiledPattern.search(" ".join( 1248 [instrument["ticker"], instrument["figi"], instrument["name"]] 1249 )) 1250 1251 if searchResult: 1252 searchResults[iType][instrument["ticker"]] = instrument 1253 1254 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1255 info = [ 1256 "# Search results\n\n", 1257 "* **Search pattern:** [{}]\n".format(pattern), 1258 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1259 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1260 ] 1261 infoShort = info[:] 1262 1263 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1264 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1265 skippedLine = "| ... | ... | ... | ... |\n" 1266 1267 if resultsLen == 0: 1268 info.append("\nNo results\n") 1269 infoShort.append("\nNo results\n") 1270 uLogger.warning("No results. Try changing your search pattern.") 1271 1272 else: 1273 for iType in searchResults: 1274 iTypeValuesCount = len(searchResults[iType].values()) 1275 if iTypeValuesCount > 0: 1276 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1277 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1278 1279 for instrument in searchResults[iType].values(): 1280 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1281 instrument["type"], 1282 instrument["ticker"], 1283 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1284 instrument["figi"], 1285 )) 1286 1287 if iTypeValuesCount <= 5: 1288 infoShort.extend(info[-iTypeValuesCount:]) 1289 1290 else: 1291 infoShort.extend(info[-5:]) 1292 infoShort.append(skippedLine) 1293 1294 infoText = "".join(info) 1295 infoTextShort = "".join(infoShort) 1296 1297 if show: 1298 uLogger.info(infoTextShort) 1299 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1300 1301 if self.searchResultsFile: 1302 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1303 fH.write(infoText) 1304 1305 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1306 1307 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1309 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1310 """ 1311 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1312 1313 :param instruments: list of strings with tickers or FIGIs. 1314 :return: list with unique instrument FIGIs only. 1315 """ 1316 requestedInstruments = [] 1317 for iName in instruments: 1318 if iName not in self.aliases.keys(): 1319 if iName not in requestedInstruments: 1320 requestedInstruments.append(iName) 1321 1322 else: 1323 if iName not in requestedInstruments: 1324 if self.aliases[iName] not in requestedInstruments: 1325 requestedInstruments.append(self.aliases[iName]) 1326 1327 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1328 1329 onlyUniqueFIGIs = [] 1330 for iName in requestedInstruments: 1331 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1332 continue 1333 1334 self._ticker = iName 1335 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1336 1337 if not iData: 1338 self._ticker = "" 1339 self._figi = iName 1340 1341 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1342 1343 if not iData: 1344 self._figi = "" 1345 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1346 1347 if iData and iData["figi"] not in onlyUniqueFIGIs: 1348 onlyUniqueFIGIs.append(iData["figi"]) 1349 1350 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1351 1352 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1354 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1355 """ 1356 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1357 1358 See limits: https://tinkoff.github.io/investAPI/limits/ 1359 1360 If `pricesFile` string is not empty then also save information to this file. 1361 1362 :param instruments: list of strings with tickers or FIGIs. 1363 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1364 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1365 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1366 """ 1367 if instruments is None or not instruments: 1368 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1369 raise Exception("Ticker or FIGI required") 1370 1371 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1372 1373 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1374 1375 iList = [] # trying to get info and current prices about all unique instruments: 1376 for self._figi in onlyUniqueFIGIs: 1377 iData = self.SearchByFIGI(requestPrice=True) 1378 iList.append(iData) 1379 1380 self.ShowListOfPrices(iList, show) 1381 1382 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1384 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1385 """ 1386 Show table contains current prices of given instruments. 1387 1388 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1389 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1390 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1391 :return: multilines text in Markdown format as a table contains current prices. 1392 """ 1393 infoText = "" 1394 1395 if show or self.pricesFile: 1396 info = [ 1397 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1398 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1399 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1400 ] 1401 1402 for item in iList: 1403 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1404 item["ticker"], 1405 item["figi"], 1406 item["type"], 1407 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1408 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1409 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1410 "{} / {}".format( 1411 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1412 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1413 ), 1414 "{} / {}".format( 1415 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1416 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1417 ), 1418 item["currency"], 1419 )) 1420 1421 infoText = "".join(info) 1422 1423 if show: 1424 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1425 1426 if self.pricesFile: 1427 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1428 fH.write(infoText) 1429 1430 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1431 1432 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1434 def RequestTradingStatus(self) -> dict: 1435 """ 1436 Requesting trading status for the instrument defined by `figi` variable. 1437 1438 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1439 1440 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1441 1442 :return: dictionary with trading status attributes. Response example: 1443 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1444 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1445 """ 1446 if self._figi is None or not self._figi: 1447 uLogger.error("Variable `figi` must be defined for using this method!") 1448 raise Exception("FIGI required") 1449 1450 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1451 1452 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1453 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1454 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1455 1456 if self.moreDebug: 1457 uLogger.debug("Records about current trading status successfully received") 1458 1459 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1461 def RequestPortfolio(self) -> dict: 1462 """ 1463 Requesting actual user's portfolio for current `accountId`. 1464 1465 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1466 1467 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1468 1469 :return: dictionary with user's portfolio. 1470 """ 1471 if self.accountId is None or not self.accountId: 1472 uLogger.error("Variable `accountId` must be defined for using this method!") 1473 raise Exception("Account ID required") 1474 1475 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1476 1477 self.body = str({"accountId": self.accountId}) 1478 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1479 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1480 1481 if self.moreDebug: 1482 uLogger.debug("Records about user's portfolio successfully received") 1483 1484 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1486 def RequestPositions(self) -> dict: 1487 """ 1488 Requesting open positions by currencies and instruments for current `accountId`. 1489 1490 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1491 1492 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1493 1494 :return: dictionary with open positions by instruments. 1495 """ 1496 if self.accountId is None or not self.accountId: 1497 uLogger.error("Variable `accountId` must be defined for using this method!") 1498 raise Exception("Account ID required") 1499 1500 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1501 1502 self.body = str({"accountId": self.accountId}) 1503 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1504 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1505 1506 if self.moreDebug: 1507 uLogger.debug("Records about current open positions successfully received") 1508 1509 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1511 def RequestPendingOrders(self) -> list: 1512 """ 1513 Requesting current actual pending limit orders for current `accountId`. 1514 1515 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1516 1517 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1518 1519 :return: list of dictionaries with pending limit orders. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1529 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1530 1531 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1532 1533 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1535 def RequestStopOrders(self) -> list: 1536 """ 1537 Requesting current actual stop orders for current `accountId`. 1538 1539 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1540 1541 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1542 1543 :return: list of dictionaries with stop orders. 1544 """ 1545 if self.accountId is None or not self.accountId: 1546 uLogger.error("Variable `accountId` must be defined for using this method!") 1547 raise Exception("Account ID required") 1548 1549 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1550 1551 self.body = str({"accountId": self.accountId}) 1552 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1553 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1554 1555 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1556 1557 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1559 def Overview(self, show: bool = False, details: str = "full") -> dict: 1560 """ 1561 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1562 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1563 and `overviewBondsCalendarFile` are defined then also save information to file. 1564 1565 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1566 many requests about the state of the portfolio, and then, based on the received data, a large number 1567 of calculation and statistics are collected. 1568 1569 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1570 :param details: how detailed should the information be? 1571 - `full` — shows full available information about portfolio status (by default), 1572 - `positions` — shows only open positions, 1573 - `orders` — shows only sections of open limits and stop orders. 1574 - `digest` — show a short digest of the portfolio status, 1575 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1576 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1577 :return: dictionary with client's raw portfolio and some statistics. 1578 """ 1579 if self.accountId is None or not self.accountId: 1580 uLogger.error("Variable `accountId` must be defined for using this method!") 1581 raise Exception("Account ID required") 1582 1583 view = { 1584 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1585 "headers": {}, # list of dictionaries, response headers without "positions" section 1586 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1587 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1588 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1589 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1590 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1591 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1592 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1593 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1594 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1595 }, 1596 "stat": { # --- some statistics calculated using "raw" sections: 1597 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1598 "availableRUB": 0., # available rubles (without other currencies) 1599 "blockedRUB": 0., # blocked sum in Russian Rouble 1600 "totalChangesRUB": 0., # changes for all open trades in RUB 1601 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1602 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1603 "sharesCostRUB": 0., # costs of all shares in RUB 1604 "bondsCostRUB": 0., # costs of all bonds in RUB 1605 "etfsCostRUB": 0., # costs of all etfs in RUB 1606 "futuresCostRUB": 0., # costs of all futures in RUB 1607 "Currencies": [], # list of dictionaries of all currencies statistics 1608 "Shares": [], # list of dictionaries of all shares statistics 1609 "Bonds": [], # list of dictionaries of all bonds statistics 1610 "Etfs": [], # list of dictionaries of all etfs statistics 1611 "Futures": [], # list of dictionaries of all futures statistics 1612 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1613 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1614 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1615 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1616 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1617 }, 1618 "analytics": { # --- some analytics of portfolio: 1619 "distrByAssets": {}, # portfolio distribution by assets 1620 "distrByCompanies": {}, # portfolio distribution by companies 1621 "distrBySectors": {}, # portfolio distribution by sectors 1622 "distrByCurrencies": {}, # portfolio distribution by currencies 1623 "distrByCountries": {}, # portfolio distribution by countries 1624 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1625 } 1626 } 1627 1628 details = details.lower() 1629 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1630 if details not in availableDetails: 1631 details = "full" 1632 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1633 1634 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1635 1636 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1637 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1638 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1639 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1640 1641 # save response headers without "positions" section: 1642 for key in portfolioResponse.keys(): 1643 if key != "positions": 1644 view["raw"]["headers"][key] = portfolioResponse[key] 1645 1646 else: 1647 continue 1648 1649 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1650 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1651 for item in portfolioResponse["positions"]: 1652 if item["instrumentType"] == "currency": 1653 self._figi = item["figi"] 1654 curr = self.SearchByFIGI(requestPrice=False) 1655 1656 # current price of currency in RUB: 1657 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1658 "name": curr["name"], 1659 "currentPrice": NanoToFloat( 1660 item["currentPrice"]["units"], 1661 item["currentPrice"]["nano"] 1662 ), 1663 } 1664 1665 view["raw"]["Currencies"].append(item) 1666 1667 elif item["instrumentType"] == "share": 1668 view["raw"]["Shares"].append(item) 1669 1670 elif item["instrumentType"] == "bond": 1671 view["raw"]["Bonds"].append(item) 1672 1673 elif item["instrumentType"] == "etf": 1674 view["raw"]["Etfs"].append(item) 1675 1676 elif item["instrumentType"] == "futures": 1677 view["raw"]["Futures"].append(item) 1678 1679 else: 1680 continue 1681 1682 # how many volume of currencies (by ISO currency name) are blocked: 1683 for item in view["raw"]["positions"]["blocked"]: 1684 blocked = NanoToFloat(item["units"], item["nano"]) 1685 if blocked > 0: 1686 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1687 1688 # how many volume of instruments (by FIGI) are blocked: 1689 for item in view["raw"]["positions"]["securities"]: 1690 blocked = int(item["blocked"]) 1691 if blocked > 0: 1692 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1693 1694 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1695 1696 if "rub" in allBlocked.keys(): 1697 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1698 1699 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1700 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1701 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1702 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1703 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1704 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1705 view["stat"]["portfolioCostRUB"] = sum([ 1706 view["stat"]["allCurrenciesCostRUB"], 1707 view["stat"]["sharesCostRUB"], 1708 view["stat"]["bondsCostRUB"], 1709 view["stat"]["etfsCostRUB"], 1710 view["stat"]["futuresCostRUB"], 1711 ]) 1712 1713 # --- calculating some portfolio statistics: 1714 byComp = {} # distribution by companies 1715 bySect = {} # distribution by sectors 1716 byCurr = {} # distribution by currencies (include RUB) 1717 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1718 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1719 1720 for item in portfolioResponse["positions"]: 1721 self._figi = item["figi"] 1722 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1723 1724 if instrument: 1725 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1726 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1727 1728 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1729 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1730 1731 else: 1732 blocked = 0 1733 1734 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1735 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1736 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1737 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1738 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1739 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1740 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1741 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1742 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1743 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1744 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1745 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1746 1747 statData = { 1748 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1749 "ticker": instrument["ticker"], # ticker by FIGI 1750 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1751 "volume": volume, # available volume of instrument 1752 "lots": lots, # volume in lots of instrument 1753 "direction": direction, # direction of an instrument's position: short or long 1754 "blocked": blocked, # blocked volume of currency or instrument 1755 "currentPrice": curPrice, # current instrument's price in basic asset 1756 "average": average, # current average position price 1757 "cost": cost, # current cost of all volume of instrument in basic asset 1758 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1759 "costRUB": costRUB, # cost of instrument in ruble 1760 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1761 "profit": profit, # expected profit at current moment 1762 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1763 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1764 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1765 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1766 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1767 "step": instrument["step"], # minimum price increment 1768 } 1769 1770 # adding distribution by unique countries: 1771 if statData["country"] not in byCountry.keys(): 1772 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1773 1774 else: 1775 byCountry[statData["country"]]["cost"] += costRUB 1776 byCountry[statData["country"]]["percent"] += percentCostRUB 1777 1778 if item["instrumentType"] != "currency": 1779 # adding distribution by unique companies: 1780 if statData["name"]: 1781 if statData["name"] not in byComp.keys(): 1782 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1783 1784 else: 1785 byComp[statData["name"]]["cost"] += costRUB 1786 byComp[statData["name"]]["percent"] += percentCostRUB 1787 1788 # adding distribution by unique sectors: 1789 if statData["sector"] not in bySect.keys(): 1790 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1791 1792 else: 1793 bySect[statData["sector"]]["cost"] += costRUB 1794 bySect[statData["sector"]]["percent"] += percentCostRUB 1795 1796 # adding distribution by unique currencies: 1797 if currency not in byCurr.keys(): 1798 byCurr[currency] = { 1799 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1800 "cost": costRUB, 1801 "percent": percentCostRUB 1802 } 1803 1804 else: 1805 byCurr[currency]["cost"] += costRUB 1806 byCurr[currency]["percent"] += percentCostRUB 1807 1808 # saving statistics for every instrument: 1809 if item["instrumentType"] == "currency": 1810 view["stat"]["Currencies"].append(statData) 1811 1812 # update dict with free funds for trading (total - blocked) by currencies 1813 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1814 view["stat"]["funds"][currency] = { 1815 "total": volume, 1816 "totalCostRUB": costRUB, # total volume cost in rubles 1817 "free": volume - blocked, 1818 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1819 } 1820 1821 elif item["instrumentType"] == "share": 1822 view["stat"]["Shares"].append(statData) 1823 1824 elif item["instrumentType"] == "bond": 1825 view["stat"]["Bonds"].append(statData) 1826 1827 elif item["instrumentType"] == "etf": 1828 view["stat"]["Etfs"].append(statData) 1829 1830 elif item["instrumentType"] == "Futures": 1831 view["stat"]["Futures"].append(statData) 1832 1833 else: 1834 continue 1835 1836 # total changes in Russian Ruble: 1837 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1838 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1839 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1840 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1841 view["stat"]["funds"]["rub"] = { 1842 "total": view["stat"]["availableRUB"], 1843 "totalCostRUB": view["stat"]["availableRUB"], 1844 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1845 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1846 } 1847 1848 # --- pending limit orders sector data: 1849 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1850 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1851 1852 for item in view["raw"]["orders"]: 1853 self._figi = item["figi"] 1854 1855 if item["figi"] not in uniquePendingOrdersFIGIs: 1856 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1857 1858 uniquePendingOrdersFIGIs.append(item["figi"]) 1859 uniquePendingOrders[item["figi"]] = instrument 1860 1861 else: 1862 instrument = uniquePendingOrders[item["figi"]] 1863 1864 if instrument: 1865 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1866 orderType = TKS_ORDER_TYPES[item["orderType"]] 1867 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1868 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1869 1870 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1871 if item["direction"] == "ORDER_DIRECTION_BUY": 1872 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1873 1874 else: 1875 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1876 1877 # requested price for order execution: 1878 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1879 1880 # necessary changes in percent to reach target from current price: 1881 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1882 1883 view["stat"]["orders"].append({ 1884 "orderID": item["orderId"], # orderId number parameter of current order 1885 "figi": item["figi"], # FIGI identification 1886 "ticker": instrument["ticker"], # ticker name by FIGI 1887 "lotsRequested": item["lotsRequested"], # requested lots value 1888 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1889 "currentPrice": lastPrice, # current instrument's price for defined action 1890 "targetPrice": target, # requested price for order execution in base currency 1891 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1892 "percentChanges": changes, # changes in percent to target from current price 1893 "currency": item["currency"], # instrument's currency name 1894 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1895 "type": orderType, # type of order from TKS_ORDER_TYPES 1896 "status": orderState, # order status from TKS_ORDER_STATES 1897 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1898 }) 1899 1900 # --- stop orders sector data: 1901 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1902 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1903 1904 for item in view["raw"]["stopOrders"]: 1905 self._figi = item["figi"] 1906 1907 if item["figi"] not in uniqueStopOrdersFIGIs: 1908 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1909 1910 uniqueStopOrdersFIGIs.append(item["figi"]) 1911 uniqueStopOrders[item["figi"]] = instrument 1912 1913 else: 1914 instrument = uniqueStopOrders[item["figi"]] 1915 1916 if instrument: 1917 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1918 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1919 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1920 1921 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1922 if "expirationTime" in item.keys(): 1923 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1924 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1925 1926 else: 1927 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1928 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1929 1930 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1931 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1932 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1933 1934 else: 1935 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1936 1937 # requested price when stop-order executed: 1938 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1939 1940 # price for limit-order, set up when stop-order executed: 1941 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1942 1943 # necessary changes in percent to reach target from current price: 1944 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1945 1946 view["stat"]["stopOrders"].append({ 1947 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1948 "figi": item["figi"], # FIGI identification 1949 "ticker": instrument["ticker"], # ticker name by FIGI 1950 "lotsRequested": item["lotsRequested"], # requested lots value 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for stop-order execution in base currency 1953 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1954 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1955 "percentChanges": changes, # changes in percent to target from current price 1956 "currency": item["currency"], # instrument's currency name 1957 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1958 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1959 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1960 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1961 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1962 }) 1963 1964 # --- calculating data for analytics section: 1965 # portfolio distribution by assets: 1966 view["analytics"]["distrByAssets"] = { 1967 "Ruble": { 1968 "uniques": 1, 1969 "cost": view["stat"]["availableRUB"], 1970 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1971 }, 1972 "Currencies": { 1973 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1974 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1975 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1976 }, 1977 "Shares": { 1978 "uniques": len(view["stat"]["Shares"]), 1979 "cost": view["stat"]["sharesCostRUB"], 1980 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1981 }, 1982 "Bonds": { 1983 "uniques": len(view["stat"]["Bonds"]), 1984 "cost": view["stat"]["bondsCostRUB"], 1985 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1986 }, 1987 "Etfs": { 1988 "uniques": len(view["stat"]["Etfs"]), 1989 "cost": view["stat"]["etfsCostRUB"], 1990 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1991 }, 1992 "Futures": { 1993 "uniques": len(view["stat"]["Futures"]), 1994 "cost": view["stat"]["futuresCostRUB"], 1995 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1996 }, 1997 } 1998 1999 # portfolio distribution by companies: 2000 view["analytics"]["distrByCompanies"]["All money cash"] = { 2001 "ticker": "", 2002 "cost": view["stat"]["allCurrenciesCostRUB"], 2003 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 } 2005 view["analytics"]["distrByCompanies"].update(byComp) 2006 2007 # portfolio distribution by sectors: 2008 view["analytics"]["distrBySectors"]["All money cash"] = { 2009 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2010 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2011 } 2012 view["analytics"]["distrBySectors"].update(bySect) 2013 2014 # portfolio distribution by currencies: 2015 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2016 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2017 2018 if self.moreDebug: 2019 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2020 2021 view["analytics"]["distrByCurrencies"].update(byCurr) 2022 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2023 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2024 2025 # portfolio distribution by countries: 2026 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2027 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2028 2029 if self.moreDebug: 2030 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2031 2032 view["analytics"]["distrByCountries"].update(byCountry) 2033 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2034 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2035 2036 # --- Prepare text statistics overview in human-readable: 2037 if show: 2038 # Whatever the value `details`, header not changes: 2039 info = [ 2040 "# Client's portfolio\n\n", 2041 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2042 "* **Account ID:** [{}]\n".format(self.accountId), 2043 ] 2044 2045 if details in ["full", "positions", "digest"]: 2046 info.extend([ 2047 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2048 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2049 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2050 view["stat"]["totalChangesRUB"], 2051 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2052 view["stat"]["totalChangesPercentRUB"], 2053 ), 2054 ]) 2055 2056 if details in ["full", "positions"]: 2057 info.extend([ 2058 "## Open positions\n\n", 2059 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2060 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2061 "| Ruble | {:>31} | | | | | |\n".format( 2062 "{:.2f} ({:.2f}) rub".format( 2063 view["stat"]["availableRUB"], 2064 view["stat"]["blockedRUB"], 2065 ) 2066 ) 2067 ]) 2068 2069 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2070 return [ 2071 "| | | | | | | |\n", 2072 "| {:<27} | | | | | {:>19} | |\n".format( 2073 noTradeStr if noTradeStr else typeStr, 2074 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2075 ), 2076 ] 2077 2078 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2079 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2080 "{} [{}]".format(data["ticker"], data["figi"]), 2081 "{:.2f} ({:.2f}) {}".format( 2082 data["volume"], 2083 data["blocked"], 2084 data["currency"], 2085 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2086 data["volume"], 2087 data["blocked"], 2088 ), 2089 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2090 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2091 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2092 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2093 "{}{:.2f} {} ({}{:.2f}%)".format( 2094 "+" if data["profit"] > 0 else "", 2095 data["profit"], data["baseCurrencyName"], 2096 "+" if data["percentProfit"] > 0 else "", 2097 data["percentProfit"], 2098 ), 2099 ) 2100 2101 # --- Show currencies section: 2102 if view["stat"]["Currencies"]: 2103 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2104 for item in view["stat"]["Currencies"]: 2105 info.append(_InfoStr(item, showCurrencyName=True)) 2106 2107 else: 2108 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2109 2110 # --- Show shares section: 2111 if view["stat"]["Shares"]: 2112 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2113 2114 for item in view["stat"]["Shares"]: 2115 info.append(_InfoStr(item)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2119 2120 # --- Show bonds section: 2121 if view["stat"]["Bonds"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2123 2124 for item in view["stat"]["Bonds"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2129 2130 # --- Show etfs section: 2131 if view["stat"]["Etfs"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2133 2134 for item in view["stat"]["Etfs"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2139 2140 # --- Show futures section: 2141 if view["stat"]["Futures"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2143 2144 for item in view["stat"]["Futures"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2149 2150 if details in ["full", "orders"]: 2151 # --- Show pending limit orders section: 2152 if view["stat"]["orders"]: 2153 info.extend([ 2154 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2155 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2156 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2157 ]) 2158 2159 for item in view["stat"]["orders"]: 2160 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2161 "{} [{}]".format(item["ticker"], item["figi"]), 2162 item["orderID"], 2163 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2164 "{} {} ({}{:.2f}%)".format( 2165 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2166 item["baseCurrencyName"], 2167 "+" if item["percentChanges"] > 0 else "", 2168 float(item["percentChanges"]), 2169 ), 2170 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2171 item["action"], 2172 item["type"], 2173 item["date"], 2174 )) 2175 2176 else: 2177 info.append("\n## Total pending limit-orders: 0\n") 2178 2179 # --- Show stop orders section: 2180 if view["stat"]["stopOrders"]: 2181 info.extend([ 2182 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2183 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2184 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2185 ]) 2186 2187 for item in view["stat"]["stopOrders"]: 2188 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2189 "{} [{}]".format(item["ticker"], item["figi"]), 2190 item["orderID"], 2191 item["lotsRequested"], 2192 "{} {} ({}{:.2f}%)".format( 2193 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2194 item["baseCurrencyName"], 2195 "+" if item["percentChanges"] > 0 else "", 2196 float(item["percentChanges"]), 2197 ), 2198 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2199 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2200 item["action"], 2201 item["type"], 2202 item["expType"], 2203 item["createDate"], 2204 item["expDate"], 2205 )) 2206 2207 else: 2208 info.append("\n## Total stop-orders: 0\n") 2209 2210 if details in ["full", "analytics"]: 2211 # -- Show analytics section: 2212 if view["stat"]["portfolioCostRUB"] > 0: 2213 info.extend([ 2214 "\n# Analytics\n" 2215 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2216 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2217 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2218 view["stat"]["totalChangesRUB"], 2219 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2220 view["stat"]["totalChangesPercentRUB"], 2221 ), 2222 "\n## Portfolio distribution by assets\n" 2223 "\n| Type | Uniques | Percent | Current cost |\n", 2224 "|------------------------------------|---------|---------|--------------------|\n", 2225 ]) 2226 2227 for key in view["analytics"]["distrByAssets"].keys(): 2228 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2229 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2230 key, 2231 view["analytics"]["distrByAssets"][key]["uniques"], 2232 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2233 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2234 )) 2235 2236 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2237 2238 info.extend([ 2239 "\n## Portfolio distribution by companies\n" 2240 "\n| Company | Percent | Current cost |\n", 2241 aSepLine, 2242 ]) 2243 2244 for company in view["analytics"]["distrByCompanies"].keys(): 2245 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2246 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2247 "{}{}".format( 2248 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2249 company, 2250 ), 2251 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2252 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2253 )) 2254 2255 info.extend([ 2256 "\n## Portfolio distribution by sectors\n" 2257 "\n| Sector | Percent | Current cost |\n", 2258 aSepLine, 2259 ]) 2260 2261 for sector in view["analytics"]["distrBySectors"].keys(): 2262 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2263 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2264 sector, 2265 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2266 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2267 )) 2268 2269 info.extend([ 2270 "\n## Portfolio distribution by currencies\n" 2271 "\n| Instruments currencies | Percent | Current cost |\n", 2272 aSepLine, 2273 ]) 2274 2275 for curr in view["analytics"]["distrByCurrencies"].keys(): 2276 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2277 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2278 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2279 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2280 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2281 )) 2282 2283 info.extend([ 2284 "\n## Portfolio distribution by countries\n" 2285 "\n| Assets by country | Percent | Current cost |\n", 2286 aSepLine, 2287 ]) 2288 2289 for country in view["analytics"]["distrByCountries"].keys(): 2290 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2291 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2292 country, 2293 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2294 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2295 )) 2296 2297 if details in ["full", "calendar"]: 2298 # -- Show bonds payment calendar section: 2299 if view["stat"]["Bonds"]: 2300 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2301 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2302 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2303 2304 else: 2305 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2306 2307 infoText = "".join(info) 2308 2309 uLogger.info(infoText) 2310 2311 if details == "full" and self.overviewFile: 2312 filename = self.overviewFile 2313 2314 elif details == "digest" and self.overviewDigestFile: 2315 filename = self.overviewDigestFile 2316 2317 elif details == "positions" and self.overviewPositionsFile: 2318 filename = self.overviewPositionsFile 2319 2320 elif details == "orders" and self.overviewOrdersFile: 2321 filename = self.overviewOrdersFile 2322 2323 elif details == "analytics" and self.overviewAnalyticsFile: 2324 filename = self.overviewAnalyticsFile 2325 2326 elif details == "calendar" and self.overviewBondsCalendarFile: 2327 filename = self.overviewBondsCalendarFile 2328 2329 else: 2330 filename = "" 2331 2332 if filename: 2333 with open(filename, "w", encoding="UTF-8") as fH: 2334 fH.write(infoText) 2335 2336 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2337 2338 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2340 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2341 """ 2342 Returns history operations between two given dates for current `accountId`. 2343 If `reportFile` string is not empty then also save human-readable report. 2344 Shows some statistical data of closed positions. 2345 2346 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2347 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2348 :param show: if `True` then also prints all records to the console. 2349 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2350 :return: original list of dictionaries with history of deals records from API ("operations" key): 2351 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2352 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2353 """ 2354 if self.accountId is None or not self.accountId: 2355 uLogger.error("Variable `accountId` must be defined for using this method!") 2356 raise Exception("Account ID required") 2357 2358 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2359 2360 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2361 2362 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2363 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2364 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2365 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2366 customStat = {} # custom statistics in additional to responseJSON 2367 2368 # --- output report in human-readable format: 2369 if show or self.reportFile: 2370 splitLine1 = "| | | | | |\n" # Summary section 2371 splitLine2 = "| | | | | | | | |\n" # Operations section 2372 nextDay = "" 2373 2374 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2375 2376 if len(ops) > 0: 2377 customStat = { 2378 "opsCount": 0, # total operations count 2379 "buyCount": 0, # buy operations 2380 "sellCount": 0, # sell operations 2381 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2382 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2383 "payIn": {"rub": 0.}, # Deposit brokerage account 2384 "payOut": {"rub": 0.}, # Withdrawals 2385 "divs": {"rub": 0.}, # Dividends income 2386 "coupons": {"rub": 0.}, # Coupon's income 2387 "brokerCom": {"rub": 0.}, # Service commissions 2388 "serviceCom": {"rub": 0.}, # Service commissions 2389 "marginCom": {"rub": 0.}, # Margin commissions 2390 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2391 } 2392 2393 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2394 for item in ops: 2395 if item["state"] == "OPERATION_STATE_EXECUTED": 2396 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2397 2398 # count buy operations: 2399 if "_BUY" in item["operationType"]: 2400 customStat["buyCount"] += 1 2401 2402 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2403 customStat["buyTotal"][item["payment"]["currency"]] += payment 2404 2405 else: 2406 customStat["buyTotal"][item["payment"]["currency"]] = payment 2407 2408 # count sell operations: 2409 elif "_SELL" in item["operationType"]: 2410 customStat["sellCount"] += 1 2411 2412 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2413 customStat["sellTotal"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["sellTotal"][item["payment"]["currency"]] = payment 2417 2418 # count incoming operations: 2419 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2420 if item["payment"]["currency"] in customStat["payIn"].keys(): 2421 customStat["payIn"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["payIn"][item["payment"]["currency"]] = payment 2425 2426 # count withdrawals operations: 2427 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2428 if item["payment"]["currency"] in customStat["payOut"].keys(): 2429 customStat["payOut"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["payOut"][item["payment"]["currency"]] = payment 2433 2434 # count dividends income: 2435 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2436 if item["payment"]["currency"] in customStat["divs"].keys(): 2437 customStat["divs"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["divs"][item["payment"]["currency"]] = payment 2441 2442 # count coupon's income: 2443 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2444 if item["payment"]["currency"] in customStat["coupons"].keys(): 2445 customStat["coupons"][item["payment"]["currency"]] += payment 2446 2447 else: 2448 customStat["coupons"][item["payment"]["currency"]] = payment 2449 2450 # count broker commissions: 2451 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2452 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2453 customStat["brokerCom"][item["payment"]["currency"]] += payment 2454 2455 else: 2456 customStat["brokerCom"][item["payment"]["currency"]] = payment 2457 2458 # count service commissions: 2459 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2460 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2461 customStat["serviceCom"][item["payment"]["currency"]] += payment 2462 2463 else: 2464 customStat["serviceCom"][item["payment"]["currency"]] = payment 2465 2466 # count margin commissions: 2467 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2468 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2469 customStat["marginCom"][item["payment"]["currency"]] += payment 2470 2471 else: 2472 customStat["marginCom"][item["payment"]["currency"]] = payment 2473 2474 # count withholding taxes: 2475 elif "_TAX" in item["operationType"]: 2476 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2477 customStat["allTaxes"][item["payment"]["currency"]] += payment 2478 2479 else: 2480 customStat["allTaxes"][item["payment"]["currency"]] = payment 2481 2482 else: 2483 continue 2484 2485 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2486 2487 # --- view "Actions" lines: 2488 info.extend([ 2489 "| Report sections | | | | |\n", 2490 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2491 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2492 "| | Buy: {:<22} | {:<28} | | |\n".format( 2493 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2494 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2495 ), 2496 "| | Sell: {:<21} | {:<28} | | |\n".format( 2497 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2498 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2499 ), 2500 ]) 2501 2502 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2503 for key in opsKeys: 2504 if key == "rub": 2505 continue 2506 2507 info.extend([ 2508 "| | | {:<28} | | |\n".format( 2509 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2510 ), 2511 "| | | {:<28} | | |\n".format( 2512 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2513 ), 2514 ]) 2515 2516 info.append(splitLine1) 2517 2518 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2519 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2520 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2521 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2522 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2523 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2524 ) 2525 2526 # --- view "Payments" lines: 2527 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2528 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2529 2530 for key in paymentsKeys: 2531 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2532 2533 info.append(splitLine1) 2534 2535 # --- view "Commissions and taxes" lines: 2536 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2537 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2538 2539 for key in comKeys: 2540 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2541 2542 info.append(splitLine1) 2543 2544 info.extend([ 2545 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2546 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2547 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2548 ]) 2549 2550 else: 2551 info.append("Broker returned no operations during this period\n") 2552 2553 # --- view "Operations" section: 2554 for item in ops: 2555 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2556 continue 2557 2558 else: 2559 self._figi = item["figi"] if item["figi"] else "" 2560 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2561 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2562 2563 # group of deals during one day: 2564 if nextDay and item["date"].split("T")[0] != nextDay: 2565 info.append(splitLine2) 2566 nextDay = "" 2567 2568 else: 2569 nextDay = item["date"].split("T")[0] # saving current day for splitting 2570 2571 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2572 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2573 self._figi if self._figi else "—", 2574 instrument["ticker"] if instrument else "—", 2575 instrument["type"] if instrument else "—", 2576 item["quantity"] if int(item["quantity"]) > 0 else "—", 2577 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2578 TKS_OPERATION_STATES[item["state"]], 2579 TKS_OPERATION_TYPES[item["operationType"]], 2580 )) 2581 2582 infoText = "".join(info) 2583 2584 if show: 2585 if self.moreDebug: 2586 uLogger.debug("Records about history of a client's operations successfully received") 2587 2588 uLogger.info(infoText) 2589 2590 if self.reportFile: 2591 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2592 fH.write(infoText) 2593 2594 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2595 2596 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2598 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2599 """ 2600 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2601 2602 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2603 Warning! Broker server used ISO UTC time by default. 2604 2605 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2606 Also, `historyFile` used to update history with `onlyMissing` parameter. 2607 2608 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2609 2610 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2611 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2612 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2613 `"hour"`, `"day"`. Default: `"hour"`. 2614 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2615 False by default. Warning! History appends only from last candle to current time 2616 with always update last candle! 2617 :param csvSep: separator if csv-file is used, `,` by default. 2618 :param show: if `True` then also prints Pandas DataFrame to the console. 2619 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2620 `["date", "time", "open", "high", "low", "close", "volume"]`. 2621 """ 2622 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2623 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2624 history = None # empty pandas object for history 2625 2626 if interval not in TKS_CANDLE_INTERVALS.keys(): 2627 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2628 raise Exception("Incorrect value") 2629 2630 if not (self._ticker or self._figi): 2631 uLogger.error("Ticker or FIGI must be defined!") 2632 raise Exception("Ticker or FIGI required") 2633 2634 if self._ticker and not self._figi: 2635 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2636 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2637 2638 if self._figi and not self._ticker: 2639 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2640 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2641 2642 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2643 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2644 if interval.lower() != "day": 2645 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2646 2647 delta = dtEnd - dtStart # current UTC time minus last time in file 2648 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2649 2650 # calculate history length in candles: 2651 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2652 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2653 length += 1 # to avoid fraction time 2654 2655 # calculate data blocks count: 2656 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2657 2658 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2659 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2660 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2661 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2662 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2663 2664 tempOld = None # pandas object for old history, if --only-missing key present 2665 lastTime = None # datetime object of last old candle in file 2666 2667 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2668 uLogger.debug("--only-missing key present, add only last missing candles...") 2669 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2670 2671 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2672 2673 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2674 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2675 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2676 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2677 2678 # get last datetime object from last string in file or minus 1 delta if file is empty: 2679 if len(tempOld) > 0: 2680 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2681 2682 else: 2683 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2684 2685 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2686 2687 responseJSONs = [] # raw history blocks of data 2688 2689 blockEnd = dtEnd 2690 for item in range(blocks): 2691 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2692 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2693 2694 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2695 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2696 )) 2697 2698 if blockStart == blockEnd: 2699 uLogger.debug("Skipped this zero-length block...") 2700 2701 else: 2702 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2703 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2704 self.body = str({ 2705 "figi": self._figi, 2706 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2707 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2708 "interval": TKS_CANDLE_INTERVALS[interval][0] 2709 }) 2710 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2711 2712 if "code" in responseJSON.keys(): 2713 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2714 2715 else: 2716 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2717 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2718 2719 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2720 2721 blockEnd = blockStart 2722 2723 printCount = len(responseJSONs) # candles to show in console 2724 if responseJSONs: 2725 tempHistory = pd.DataFrame( 2726 data={ 2727 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2728 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2729 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2730 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2731 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2732 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2733 "volume": [int(item["volume"]) for item in responseJSONs], 2734 }, 2735 index=range(len(responseJSONs)), 2736 columns=["date", "time", "open", "high", "low", "close", "volume"], 2737 ) 2738 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2739 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2740 2741 # append only newest candles to old history if --only-missing key present: 2742 if onlyMissing and tempOld is not None and lastTime is not None: 2743 index = 0 # find start index in tempHistory data: 2744 2745 for i, item in tempHistory.iterrows(): 2746 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2747 2748 if curTime == lastTime: 2749 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2750 index = i 2751 printCount = index + 1 2752 break 2753 2754 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2755 2756 else: 2757 history = tempHistory # if no `--only-missing` key then load full data from server 2758 2759 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2760 2761 if history is not None and not history.empty: 2762 if show: 2763 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2764 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2765 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2766 )) 2767 2768 else: 2769 uLogger.warning("Received an empty candles history!") 2770 2771 if self.historyFile is not None: 2772 if history is not None and not history.empty: 2773 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2774 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2775 2776 else: 2777 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2778 2779 else: 2780 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2781 2782 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2784 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2785 """ 2786 Load candles history from csv-file and return Pandas DataFrame object. 2787 2788 See also: `History()` and `ShowHistoryChart()` methods. 2789 2790 :param filePath: path to csv-file to open. 2791 """ 2792 loadedHistory = None # init candles data object 2793 2794 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2795 2796 if os.path.exists(filePath): 2797 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2798 2799 tfStr = self.priceModel.FormattedDelta( 2800 self.priceModel.timeframe, 2801 "{days} days {hours}h {minutes}m {seconds}s", 2802 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2803 self.priceModel.timeframe, 2804 "{hours}h {minutes}m {seconds}s", 2805 ) 2806 2807 if loadedHistory is not None and not loadedHistory.empty: 2808 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2809 len(loadedHistory), 2810 tfStr, 2811 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2812 ) 2813 2814 else: 2815 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2816 2817 else: 2818 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2819 2820 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2822 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2823 """ 2824 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2825 2826 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2827 Default: `index.html` (both for interact and non-interact candlesticks chart). 2828 2829 See also: `History()` and `LoadHistory()` methods. 2830 2831 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2832 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2833 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2834 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2835 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2836 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2837 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2838 """ 2839 if isinstance(candles, str): 2840 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2841 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2842 2843 elif isinstance(candles, pd.DataFrame): 2844 self.priceModel.prices = candles # set candles chain from variable 2845 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2846 2847 if "datetime" not in candles.columns: 2848 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2849 2850 else: 2851 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2852 raise Exception("Incorrect value") 2853 2854 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2855 2856 if interact: 2857 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2858 2859 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2860 2861 else: 2862 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2863 2864 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2865 2866 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2868 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2869 """ 2870 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2871 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2872 2873 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2874 2875 :param operation: string "Buy" or "Sell". 2876 :param lots: volume, integer count of lots >= 1. 2877 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2878 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2879 :param expDate: string "Undefined" by default or local date in future, 2880 it is a string with format `%Y-%m-%d %H:%M:%S`. 2881 :return: JSON with response from broker server. 2882 """ 2883 if self.accountId is None or not self.accountId: 2884 uLogger.error("Variable `accountId` must be defined for using this method!") 2885 raise Exception("Account ID required") 2886 2887 if operation is None or not operation or operation not in ("Buy", "Sell"): 2888 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2889 raise Exception("Incorrect value") 2890 2891 if lots is None or lots < 1: 2892 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2893 lots = 1 2894 2895 if tp is None or tp < 0: 2896 tp = 0 2897 2898 if sl is None or sl < 0: 2899 sl = 0 2900 2901 if expDate is None or not expDate: 2902 expDate = "Undefined" 2903 2904 if not (self._ticker or self._figi): 2905 uLogger.error("Ticker or FIGI must be defined!") 2906 raise Exception("Ticker or FIGI required") 2907 2908 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 2909 self._ticker = instrument["ticker"] 2910 self._figi = instrument["figi"] 2911 2912 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 2913 2914 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2915 self.body = str({ 2916 "figi": self._figi, 2917 "quantity": str(lots), 2918 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2919 "accountId": str(self.accountId), 2920 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2921 }) 2922 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2923 2924 if "orderId" in response.keys(): 2925 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2926 operation, response["orderId"], 2927 self._ticker, self._figi, lots, 2928 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2929 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2930 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2931 )) 2932 2933 if tp > 0: 2934 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2935 2936 if sl > 0: 2937 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2938 2939 else: 2940 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 2941 2942 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2944 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2945 """ 2946 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2947 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2948 2949 See also: `Order()` and `Trade()` docstrings. 2950 2951 :param lots: volume, integer count of lots >= 1. 2952 :param tp: float > 0, take profit price of stop-order. 2953 :param sl: float > 0, stop loss price of stop-order. 2954 :param expDate: it's a local date in future. 2955 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2956 :return: JSON with response from broker server. 2957 """ 2958 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2960 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2961 """ 2962 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2963 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2964 2965 See also: `Order()` and `Trade()` docstrings. 2966 2967 :param lots: volume, integer count of lots >= 1. 2968 :param tp: float > 0, take profit price of stop-order. 2969 :param sl: float > 0, stop loss price of stop-order. 2970 :param expDate: it's a local date in the future. 2971 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2972 :return: JSON with response from broker server. 2973 """ 2974 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2976 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2977 """ 2978 Close position of given instruments. 2979 2980 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2981 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2982 This avoids unnecessary downloading data from the server. 2983 """ 2984 if instruments is None or not instruments: 2985 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2986 raise Exception("Ticker or FIGI required") 2987 2988 if isinstance(instruments, str): 2989 instruments = [instruments] 2990 2991 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2992 if uniqueInstruments: 2993 if portfolio is None or not portfolio: 2994 portfolio = self.Overview(show=False) 2995 2996 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2997 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2998 2999 for self._figi in uniqueInstruments: 3000 if self._figi not in allOpened: 3001 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3002 continue 3003 3004 # search open trade info about instrument by ticker: 3005 instrument = {} 3006 for iType in TKS_INSTRUMENTS: 3007 if instrument: 3008 break 3009 3010 for item in portfolio["stat"][iType]: 3011 if item["figi"] == self._figi: 3012 instrument = item 3013 break 3014 3015 if instrument: 3016 self._ticker = instrument["ticker"] 3017 self._figi = instrument["figi"] 3018 3019 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3020 self._ticker, 3021 self._figi, 3022 int(instrument["volume"]), 3023 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3024 )) 3025 3026 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3027 3028 if tradeLots > 0: 3029 if instrument["blocked"] > 0: 3030 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3031 instrument["blocked"], 3032 self._ticker, 3033 tradeLots, 3034 )) 3035 3036 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3037 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3038 3039 else: 3040 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3042 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3043 """ 3044 Close all positions of given instruments with defined type. 3045 3046 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3047 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3048 This avoids unnecessary downloading data from the server. 3049 """ 3050 if iType not in TKS_INSTRUMENTS: 3051 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3052 3053 else: 3054 if portfolio is None or not portfolio: 3055 portfolio = self.Overview(show=False) 3056 3057 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3058 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3059 3060 if tickers and portfolio: 3061 self.CloseTrades(tickers, portfolio) 3062 3063 else: 3064 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3066 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3067 """ 3068 Universal method to create market or limit orders with all available parameters for current `accountId`. 3069 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3070 3071 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3072 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3073 3074 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3075 then broker immediately open market order as you can do simple --buy or --sell operations! 3076 3077 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3078 When current price will go up or down to target price value then broker opens a limit order. 3079 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3080 3081 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3082 3083 :param operation: string "Buy" or "Sell". 3084 :param orderType: string "Limit" or "Stop". 3085 :param lots: volume, integer count of lots >= 1. 3086 :param targetPrice: target price > 0. This is open trade price for limit order. 3087 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3088 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3089 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3090 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3091 Stop loss order always executed by market price. 3092 :param expDate: string "Undefined" by default or local date in future. 3093 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3094 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3095 A limit order has no expiration date, it lasts until the end of the trading day. 3096 :return: JSON with response from broker server. 3097 """ 3098 if self.accountId is None or not self.accountId: 3099 uLogger.error("Variable `accountId` must be defined for using this method!") 3100 raise Exception("Account ID required") 3101 3102 if operation is None or not operation or operation not in ("Buy", "Sell"): 3103 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3104 raise Exception("Incorrect value") 3105 3106 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3107 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3108 raise Exception("Incorrect value") 3109 3110 if lots is None or lots < 1: 3111 uLogger.error("You must define trade volume > 0: integer count of lots!") 3112 raise Exception("Incorrect value") 3113 3114 if targetPrice is None or targetPrice <= 0: 3115 uLogger.error("Target price for limit-order must be greater than 0!") 3116 raise Exception("Incorrect value") 3117 3118 if limitPrice is None or limitPrice <= 0: 3119 limitPrice = targetPrice 3120 3121 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3122 stopType = "Limit" 3123 3124 if expDate is None or not expDate: 3125 expDate = "Undefined" 3126 3127 if not (self._ticker or self._figi): 3128 uLogger.error("Tocker or FIGI must be defined!") 3129 raise Exception("Ticker or FIGI required") 3130 3131 response = {} 3132 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3133 self._ticker = instrument["ticker"] 3134 self._figi = instrument["figi"] 3135 3136 if orderType == "Limit": 3137 uLogger.debug( 3138 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3139 self._ticker, self._figi, 3140 operation, lots, targetPrice, instrument["currency"], 3141 )) 3142 3143 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3144 self.body = str({ 3145 "figi": self._figi, 3146 "quantity": str(lots), 3147 "price": FloatToNano(targetPrice), 3148 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3149 "accountId": str(self.accountId), 3150 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3151 }) 3152 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3153 3154 if "orderId" in response.keys(): 3155 uLogger.info( 3156 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3157 response["orderId"], 3158 self._ticker, self._figi, 3159 operation, lots, targetPrice, instrument["currency"], 3160 )) 3161 3162 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3163 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3164 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3165 targetPrice, instrument["currency"], 3166 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3167 )) 3168 3169 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3170 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3171 targetPrice, instrument["currency"], 3172 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3173 )) 3174 3175 else: 3176 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3177 3178 if orderType == "Stop": 3179 uLogger.debug( 3180 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3181 self._ticker, self._figi, 3182 operation, lots, 3183 targetPrice, instrument["currency"], 3184 limitPrice, instrument["currency"], 3185 stopType, expDate, 3186 )) 3187 3188 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3189 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3190 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3191 3192 body = { 3193 "figi": self._figi, 3194 "quantity": str(lots), 3195 "price": FloatToNano(limitPrice), 3196 "stopPrice": FloatToNano(targetPrice), 3197 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3198 "accountId": str(self.accountId), 3199 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3200 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3201 } 3202 3203 if expDateUTC: 3204 body["expireDate"] = expDateUTC 3205 3206 self.body = str(body) 3207 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3208 3209 if "stopOrderId" in response.keys(): 3210 uLogger.info( 3211 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3212 response["stopOrderId"], 3213 self._ticker, self._figi, 3214 operation, lots, 3215 targetPrice, instrument["currency"], 3216 limitPrice, instrument["currency"], 3217 TKS_STOP_ORDER_TYPES[stopOrderType], 3218 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3219 )) 3220 3221 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3222 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3223 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3224 targetPrice, instrument["currency"], 3225 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3226 )) 3227 3228 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3229 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3230 targetPrice, instrument["currency"], 3231 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3232 )) 3233 3234 else: 3235 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3236 3237 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3239 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3240 """ 3241 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3242 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3243 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3244 See also: `Order()` docstring. 3245 3246 :param lots: volume, integer count of lots >= 1. 3247 :param targetPrice: target price > 0. This is open trade price for limit order. 3248 :return: JSON with response from broker server. 3249 """ 3250 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3252 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3253 """ 3254 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3255 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3256 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3257 target price value then broker opens a limit order. See also: `Order()` docstring. 3258 3259 :param lots: volume, integer count of lots >= 1. 3260 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3261 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3262 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3263 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3264 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3265 :param expDate: string "Undefined" by default or local date in future. 3266 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3267 This date is converting to UTC format for server. 3268 :return: JSON with response from broker server. 3269 """ 3270 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3272 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3273 """ 3274 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3275 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3276 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3277 See also: `Order()` docstring. 3278 3279 :param lots: volume, integer count of lots >= 1. 3280 :param targetPrice: target price > 0. This is open trade price for limit order. 3281 :return: JSON with response from broker server. 3282 """ 3283 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3285 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3286 """ 3287 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3288 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3289 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3290 target price value then broker opens a limit order. See also: `Order()` docstring. 3291 3292 :param lots: volume, integer count of lots >= 1. 3293 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3294 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3295 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3296 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3297 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3298 :param expDate: string "Undefined" by default or local date in future. 3299 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3300 This date is converting to UTC format for server. 3301 :return: JSON with response from broker server. 3302 """ 3303 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3305 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3306 """ 3307 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3308 3309 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3310 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3311 This avoids unnecessary downloading data from the server. 3312 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3313 """ 3314 if self.accountId is None or not self.accountId: 3315 uLogger.error("Variable `accountId` must be defined for using this method!") 3316 raise Exception("Account ID required") 3317 3318 if orderIDs: 3319 if allOrdersIDs is None: 3320 rawOrders = self.RequestPendingOrders() 3321 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3322 3323 if allStopOrdersIDs is None: 3324 rawStopOrders = self.RequestStopOrders() 3325 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3326 3327 for orderID in orderIDs: 3328 idInPendingOrders = orderID in allOrdersIDs 3329 idInStopOrders = orderID in allStopOrdersIDs 3330 3331 if not (idInPendingOrders or idInStopOrders): 3332 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3333 continue 3334 3335 else: 3336 if idInPendingOrders: 3337 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3338 3339 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3340 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3341 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3342 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3343 3344 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3345 if self.moreDebug: 3346 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3347 3348 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3349 3350 else: 3351 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3352 3353 elif idInStopOrders: 3354 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3355 3356 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3357 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3358 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3359 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3360 3361 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3362 if self.moreDebug: 3363 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3364 3365 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3366 3367 else: 3368 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3369 3370 else: 3371 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3373 def CloseAllOrders(self) -> None: 3374 """ 3375 Gets a list of open pending and stop orders and cancel it all. 3376 """ 3377 rawOrders = self.RequestPendingOrders() 3378 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3379 lenOrders = len(allOrdersIDs) 3380 3381 rawStopOrders = self.RequestStopOrders() 3382 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3383 lenSOrders = len(allStopOrdersIDs) 3384 3385 if lenOrders > 0 or lenSOrders > 0: 3386 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3387 3388 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3389 3390 else: 3391 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3393 def CloseAll(self, *args) -> None: 3394 """ 3395 Close all available (not blocked) opened trades and orders. 3396 3397 Also, you can select one or more keywords case-insensitive: 3398 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3399 3400 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3401 """ 3402 overview = self.Overview(show=False) # get all open trades info 3403 3404 if len(args) == 0: 3405 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3406 self.CloseAllOrders() # close all pending and stop orders 3407 3408 for iType in TKS_INSTRUMENTS: 3409 if iType != "Currencies": 3410 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3411 3412 else: 3413 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3414 lowerArgs = [x.lower() for x in args] 3415 3416 if "orders" in lowerArgs: 3417 self.CloseAllOrders() # close all pending and stop orders 3418 3419 for iType in TKS_INSTRUMENTS: 3420 if iType.lower() in lowerArgs and iType != "Currencies": 3421 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3423 def CloseAllByTicker(self, instrument: str) -> None: 3424 """ 3425 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3426 3427 This method searches opened trade and orders of instrument throw all portfolio and then use 3428 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3429 3430 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3431 3432 :param instrument: string with ticker. 3433 """ 3434 if instrument is None or not instrument: 3435 uLogger.error("Ticker name must be defined for using this method!") 3436 raise Exception("Ticker required") 3437 3438 overview = self.Overview(show=False) # get user portfolio with all open trades info 3439 3440 self._ticker = instrument # try to set instrument as ticker 3441 self._figi = "" 3442 3443 if self.IsInPortfolio(portfolio=overview): 3444 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3445 self.CloseTrades(instruments=[instrument], portfolio=overview) 3446 3447 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3448 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3449 3450 if limitAll and self.IsInLimitOrders(portfolio=overview): 3451 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3452 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3453 3454 if stopAll and self.IsInStopOrders(portfolio=overview): 3455 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3456 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3458 def CloseAllByFIGI(self, instrument: str) -> None: 3459 """ 3460 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3461 3462 This method searches opened trade and orders of instrument throw all portfolio and then use 3463 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3464 3465 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3466 3467 :param instrument: string with FIGI id. 3468 """ 3469 if instrument is None or not instrument: 3470 uLogger.error("FIGI id must be defined for using this method!") 3471 raise Exception("FIGI required") 3472 3473 overview = self.Overview(show=False) # get user portfolio with all open trades info 3474 3475 self._ticker = "" 3476 self._figi = instrument # try to set instrument as FIGI id 3477 3478 if self.IsInPortfolio(portfolio=overview): 3479 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3480 self.CloseTrades(instruments=[instrument], portfolio=overview) 3481 3482 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3483 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3484 3485 if limitAll and self.IsInLimitOrders(portfolio=overview): 3486 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3487 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3488 3489 if stopAll and self.IsInStopOrders(portfolio=overview): 3490 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3491 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3493 @staticmethod 3494 def ParseOrderParameters(operation, **inputParameters): 3495 """ 3496 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3497 3498 :param operation: string "Buy" or "Sell". 3499 :param inputParameters: this is dict of strings that looks like this 3500 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3501 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3502 "prices" key: one or more prices to open limit-orders 3503 Counts of values in lots and prices lists must be equals! 3504 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3505 """ 3506 # TODO: update order grid work with api v2 3507 pass 3508 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3509 # 3510 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3511 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3512 # raise Exception("Incorrect value") 3513 # 3514 # if "l" in inputParameters.keys(): 3515 # inputParameters["lots"] = inputParameters.pop("l") 3516 # 3517 # if "p" in inputParameters.keys(): 3518 # inputParameters["prices"] = inputParameters.pop("p") 3519 # 3520 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3521 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3522 # raise Exception("Incorrect value") 3523 # 3524 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3525 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3526 # 3527 # if len(lots) != len(prices): 3528 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3529 # raise Exception("Incorrect value") 3530 # 3531 # uLogger.debug("Extracted parameters for orders:") 3532 # uLogger.debug("lots = {}".format(lots)) 3533 # uLogger.debug("prices = {}".format(prices)) 3534 # 3535 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3536 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3537 # uLogger.debug("Order parameters: {}".format(result)) 3538 # 3539 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3541 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3542 """ 3543 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3544 3545 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3546 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3547 """ 3548 result = False 3549 msg = "Instrument not defined!" 3550 3551 if portfolio is None or not portfolio: 3552 portfolio = self.Overview(show=False) 3553 3554 if self._ticker: 3555 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3556 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3557 3558 for iType in TKS_INSTRUMENTS: 3559 for instrument in portfolio["stat"][iType]: 3560 if instrument["ticker"] == self._ticker: 3561 result = True 3562 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3563 break 3564 3565 elif self._figi: 3566 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3567 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3568 3569 for iType in TKS_INSTRUMENTS: 3570 for instrument in portfolio["stat"][iType]: 3571 if instrument["figi"] == self._figi: 3572 result = True 3573 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3574 break 3575 3576 else: 3577 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3578 3579 uLogger.debug(msg) 3580 3581 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3583 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3584 """ 3585 Returns instrument from the user's portfolio if it presents there. 3586 Instrument must be defined by `ticker` (highly priority) or `figi`. 3587 3588 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3589 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3590 """ 3591 result = None 3592 msg = "Instrument not defined!" 3593 3594 if portfolio is None or not portfolio: 3595 portfolio = self.Overview(show=False) 3596 3597 if self._ticker: 3598 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3599 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3600 3601 for iType in TKS_INSTRUMENTS: 3602 for instrument in portfolio["stat"][iType]: 3603 if instrument["ticker"] == self._ticker: 3604 result = instrument 3605 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3606 break 3607 3608 elif self._figi: 3609 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3610 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3611 3612 for iType in TKS_INSTRUMENTS: 3613 for instrument in portfolio["stat"][iType]: 3614 if instrument["figi"] == self._figi: 3615 result = instrument 3616 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3617 break 3618 3619 else: 3620 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3621 3622 uLogger.debug(msg) 3623 3624 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3626 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3627 """ 3628 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3629 3630 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3631 3632 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3633 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3634 """ 3635 result = False 3636 msg = "Instrument not defined!" 3637 3638 if portfolio is None or not portfolio: 3639 portfolio = self.Overview(show=False) 3640 3641 if self._ticker: 3642 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3643 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3644 3645 for instrument in portfolio["stat"]["orders"]: 3646 if instrument["ticker"] == self._ticker: 3647 result = True 3648 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3649 break 3650 3651 elif self._figi: 3652 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3653 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3654 3655 for instrument in portfolio["stat"]["orders"]: 3656 if instrument["figi"] == self._figi: 3657 result = True 3658 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3659 break 3660 3661 else: 3662 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3663 3664 uLogger.debug(msg) 3665 3666 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3668 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3669 """ 3670 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3671 Instrument must be defined by `ticker` (highly priority) or `figi`. 3672 3673 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3674 3675 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3676 :return: list with `orderID`s of limit orders. 3677 """ 3678 result = [] 3679 msg = "Instrument not defined!" 3680 3681 if portfolio is None or not portfolio: 3682 portfolio = self.Overview(show=False) 3683 3684 if self._ticker: 3685 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3686 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3687 3688 for instrument in portfolio["stat"]["orders"]: 3689 if instrument["ticker"] == self._ticker: 3690 result.append(instrument["orderID"]) 3691 3692 if result: 3693 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3694 3695 elif self._figi: 3696 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3697 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3698 3699 for instrument in portfolio["stat"]["orders"]: 3700 if instrument["figi"] == self._figi: 3701 result.append(instrument["orderID"]) 3702 3703 if result: 3704 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3705 3706 else: 3707 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3708 3709 uLogger.debug(msg) 3710 3711 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3713 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3714 """ 3715 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3716 3717 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3718 3719 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3720 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3721 """ 3722 result = False 3723 msg = "Instrument not defined!" 3724 3725 if portfolio is None or not portfolio: 3726 portfolio = self.Overview(show=False) 3727 3728 if self._ticker: 3729 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3730 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3731 3732 for instrument in portfolio["stat"]["stopOrders"]: 3733 if instrument["ticker"] == self._ticker: 3734 result = True 3735 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3736 break 3737 3738 elif self._figi: 3739 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3740 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3741 3742 for instrument in portfolio["stat"]["stopOrders"]: 3743 if instrument["figi"] == self._figi: 3744 result = True 3745 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3746 break 3747 3748 else: 3749 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3750 3751 uLogger.debug(msg) 3752 3753 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3755 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3756 """ 3757 Returns list with all `orderID`s of opened stop orders for the instrument. 3758 Instrument must be defined by `ticker` (highly priority) or `figi`. 3759 3760 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3761 3762 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3763 :return: list with `orderID`s of stop orders. 3764 """ 3765 result = [] 3766 msg = "Instrument not defined!" 3767 3768 if portfolio is None or not portfolio: 3769 portfolio = self.Overview(show=False) 3770 3771 if self._ticker: 3772 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3773 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3774 3775 for instrument in portfolio["stat"]["stopOrders"]: 3776 if instrument["ticker"] == self._ticker: 3777 result.append(instrument["orderID"]) 3778 3779 if result: 3780 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3781 3782 elif self._figi: 3783 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3784 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3785 3786 for instrument in portfolio["stat"]["stopOrders"]: 3787 if instrument["figi"] == self._figi: 3788 result.append(instrument["orderID"]) 3789 3790 if result: 3791 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3792 3793 else: 3794 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3795 3796 uLogger.debug(msg) 3797 3798 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3800 def RequestLimits(self) -> dict: 3801 """ 3802 Method for obtaining the available funds for withdrawal for current `accountId`. 3803 3804 See also: 3805 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3806 - `OverviewLimits()` method 3807 3808 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3809 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3810 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3811 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3812 """ 3813 if self.accountId is None or not self.accountId: 3814 uLogger.error("Variable `accountId` must be defined for using this method!") 3815 raise Exception("Account ID required") 3816 3817 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3818 3819 self.body = str({"accountId": self.accountId}) 3820 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3821 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3822 3823 if self.moreDebug: 3824 uLogger.debug("Records about available funds for withdrawal successfully received") 3825 3826 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3828 def OverviewLimits(self, show: bool = False) -> dict: 3829 """ 3830 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3831 3832 See also: `RequestLimits()`. 3833 3834 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3835 :return: dict with raw parsed data from server and some calculated statistics about it. 3836 """ 3837 if self.accountId is None or not self.accountId: 3838 uLogger.error("Variable `accountId` must be defined for using this method!") 3839 raise Exception("Account ID required") 3840 3841 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3842 3843 view = { 3844 "rawLimits": rawLimits, 3845 "limits": { # parsed data for every currency: 3846 "money": { # this is an array of portfolio currency positions 3847 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3848 }, 3849 "blocked": { # this is an array of blocked currency 3850 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3851 }, 3852 "blockedGuarantee": { # this is locked money under collateral for futures 3853 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3854 }, 3855 }, 3856 } 3857 3858 # --- Prepare text table with limits in human-readable format: 3859 if show: 3860 info = [ 3861 "# Withdrawal limits\n\n", 3862 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3863 "* **Account ID:** [{}]\n".format(self.accountId), 3864 ] 3865 3866 if view["limits"]["money"]: 3867 info.extend([ 3868 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3869 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3870 ]) 3871 3872 else: 3873 info.append("\nNo withdrawal limits\n") 3874 3875 for curr in view["limits"]["money"].keys(): 3876 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3877 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3878 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3879 3880 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3881 "[{}]".format(curr), 3882 "{:.2f}".format(view["limits"]["money"][curr]), 3883 "{:.2f}".format(availableMoney), 3884 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3885 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3886 ) 3887 3888 if curr == "rub": 3889 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3890 3891 else: 3892 info.append(infoStr) 3893 3894 infoText = "".join(info) 3895 3896 uLogger.info(infoText) 3897 3898 if self.withdrawalLimitsFile: 3899 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3900 fH.write(infoText) 3901 3902 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3903 3904 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3906 def RequestAccounts(self) -> dict: 3907 """ 3908 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3909 3910 See also: 3911 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3912 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3913 - `OverviewUserInfo()` method 3914 3915 :return: dict with raw data from server that contains accounts info. Example of dict: 3916 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3917 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3918 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3919 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3920 """ 3921 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3922 3923 self.body = str({}) 3924 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3925 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3926 3927 if self.moreDebug: 3928 uLogger.debug("Records about available accounts successfully received") 3929 3930 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3932 def RequestUserInfo(self) -> dict: 3933 """ 3934 Method for requesting common user's information. 3935 3936 See also: 3937 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3938 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3939 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3940 - `OverviewUserInfo()` method 3941 3942 :return: dict with raw data from server that contains user's information. Example of dict: 3943 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3944 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3945 """ 3946 uLogger.debug("Requesting common user's information. Wait, please...") 3947 3948 self.body = str({}) 3949 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3950 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3951 3952 if self.moreDebug: 3953 uLogger.debug("Records about current user successfully received") 3954 3955 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3957 def RequestMarginStatus(self, accountId: str = None) -> dict: 3958 """ 3959 Method for requesting margin calculation for defined account ID. 3960 3961 See also: 3962 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3963 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3964 - `OverviewUserInfo()` method 3965 3966 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3967 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3968 Example of responses: 3969 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3970 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3971 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3972 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3973 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3974 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3975 """ 3976 if accountId is None or not accountId: 3977 if self.accountId is None or not self.accountId: 3978 uLogger.error("Variable `accountId` must be defined for using this method!") 3979 raise Exception("Account ID required") 3980 3981 else: 3982 accountId = self.accountId # use `self.accountId` (main ID) by default 3983 3984 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3985 3986 self.body = str({"accountId": accountId}) 3987 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3988 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3989 3990 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3991 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3992 rawMargin = {} 3993 3994 else: 3995 if self.moreDebug: 3996 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3997 3998 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4000 def RequestTariffLimits(self) -> dict: 4001 """ 4002 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4003 4004 See also: 4005 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4006 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4007 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4008 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4009 - `OverviewUserInfo()` method 4010 4011 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4012 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4013 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4014 """ 4015 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4016 4017 self.body = str({}) 4018 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4019 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4020 4021 if self.moreDebug: 4022 uLogger.debug("Records with limits of current tariff successfully received") 4023 4024 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4026 def RequestBondCoupons(self, iJSON: dict) -> dict: 4027 """ 4028 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4029 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4030 All dates are in UTC timezone. 4031 4032 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4033 Documentation: 4034 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4035 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4036 4037 See also: `ExtendBondsData()`. 4038 4039 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4040 If raw iJSON is not data of bond then server returns an error [400] with message: 4041 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4042 :return: dictionary with bond payment calendar. Response example 4043 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4044 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4045 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4046 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4047 """ 4048 if iJSON["figi"] is None or not iJSON["figi"]: 4049 uLogger.error("FIGI must be defined for using this method!") 4050 raise Exception("FIGI required") 4051 4052 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4053 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4054 4055 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4056 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4057 self._figi, 4058 startDate, 4059 endDate, 4060 )) 4061 4062 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4063 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4064 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4065 4066 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4067 uLogger.warning("Instrument type is not bond!") 4068 4069 else: 4070 if self.moreDebug: 4071 uLogger.debug("Records about bond payment calendar successfully received") 4072 4073 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4075 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4076 """ 4077 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4078 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4079 coupon yields, current yields and some statistics etc. 4080 4081 WARNING! This is too long operation if a lot of bonds requested from broker server. 4082 4083 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4084 4085 :param instruments: list of strings with tickers or FIGIs. 4086 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4087 for further used by data scientists or stock analytics. 4088 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4089 In XLSX-file and Pandas DataFrame fields mean: 4090 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4091 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4092 """ 4093 if instruments is None or not instruments: 4094 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4095 raise Exception("Ticker or FIGI required") 4096 4097 if isinstance(instruments, str): 4098 instruments = [instruments] 4099 4100 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4101 4102 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4103 4104 iCount = len(uniqueInstruments) 4105 tooLong = iCount >= 20 4106 if tooLong: 4107 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4108 4109 bonds = None 4110 for i, self._figi in enumerate(uniqueInstruments): 4111 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4112 4113 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4114 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4115 rawBond = self.SearchByFIGI(requestPrice=True) 4116 4117 # Widen raw data with UTC current time (iData["actualDateTime"]): 4118 actualDate = datetime.now(tzutc()) 4119 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4120 4121 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4122 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4123 4124 # Replace some values with human-readable: 4125 iData["nominalCurrency"] = iData["nominal"]["currency"] 4126 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4127 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4128 iData["aciCurrency"] = iData["aciValue"]["currency"] 4129 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4130 iData["issueSize"] = int(iData["issueSize"]) 4131 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4132 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4133 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4134 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4135 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4136 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4137 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4138 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4139 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4140 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4141 4142 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4143 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4144 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4145 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4146 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4147 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4148 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4149 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4150 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4151 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4152 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4153 4154 # Widen raw data with calendar data from `rawCalendar` values: 4155 calendarData = [] 4156 if "events" in iData["rawCalendar"].keys(): 4157 for item in iData["rawCalendar"]["events"]: 4158 calendarData.append({ 4159 "couponDate": item["couponDate"], 4160 "couponNumber": int(item["couponNumber"]), 4161 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4162 "payCurrency": item["payOneBond"]["currency"], 4163 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4164 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4165 "couponStartDate": item["couponStartDate"], 4166 "couponEndDate": item["couponEndDate"], 4167 "couponPeriod": item["couponPeriod"], 4168 }) 4169 4170 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4171 if "maturityDate" not in iData.keys(): 4172 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4173 4174 # Widen raw data with Coupon Rate. 4175 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4176 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4177 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4178 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4179 4180 # Widen raw data with Yield to Maturity (YTM) on current date. 4181 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4182 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4183 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4184 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4185 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4186 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4187 4188 iData["calendar"] = calendarData # adds calendar at the end 4189 4190 # Remove not used data: 4191 iData.pop("uid") 4192 iData.pop("positionUid") 4193 iData.pop("currentPrice") 4194 iData.pop("rawCalendar") 4195 4196 colNames = list(iData.keys()) 4197 if bonds is None: 4198 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4199 4200 else: 4201 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4202 4203 else: 4204 uLogger.warning("Instrument is not a bond!") 4205 4206 processed = round(100 * (i + 1) / iCount, 1) 4207 if tooLong and processed % 5 == 0: 4208 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4209 4210 else: 4211 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4212 4213 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4214 4215 # Saving bonds from Pandas DataFrame to XLSX sheet: 4216 if xlsx and self.bondsXLSXFile: 4217 with pd.ExcelWriter( 4218 path=self.bondsXLSXFile, 4219 date_format=TKS_DATE_FORMAT, 4220 datetime_format=TKS_DATE_TIME_FORMAT, 4221 mode="w", 4222 ) as writer: 4223 bonds.to_excel( 4224 writer, 4225 sheet_name="Extended bonds data", 4226 index=True, 4227 encoding="UTF-8", 4228 freeze_panes=(1, 1), 4229 ) # saving as XLSX-file with freeze first row and column as headers 4230 4231 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4232 4233 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4235 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4236 """ 4237 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4238 4239 WARNING! This is too long operation if a lot of bonds requested from broker server. 4240 4241 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4242 4243 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4244 extended information about bonds: main info, current prices, bond payment calendar, 4245 coupon yields, current yields and some statistics etc. 4246 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4247 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4248 for further used by data scientists or stock analytics. 4249 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4250 """ 4251 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4252 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4253 4254 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4255 4256 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4257 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4258 calendar = None 4259 for bond in extBonds.iterrows(): 4260 for item in bond[1]["calendar"]: 4261 cData = { 4262 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4263 "couponDate": item["couponDate"], 4264 "figi": bond[1]["figi"], 4265 "ticker": bond[1]["ticker"], 4266 "name": bond[1]["name"], 4267 "couponNumber": item["couponNumber"], 4268 "payOneBond": item["payOneBond"], 4269 "payCurrency": item["payCurrency"], 4270 "couponType": item["couponType"], 4271 "couponPeriod": item["couponPeriod"], 4272 "fixDate": item["fixDate"], 4273 "couponStartDate": item["couponStartDate"], 4274 "couponEndDate": item["couponEndDate"], 4275 } 4276 4277 if calendar is None: 4278 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4279 4280 else: 4281 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4282 4283 if calendar is not None: 4284 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4285 4286 # Saving calendar from Pandas DataFrame to XLSX sheet: 4287 if xlsx: 4288 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4289 4290 with pd.ExcelWriter( 4291 path=xlsxCalendarFile, 4292 date_format=TKS_DATE_FORMAT, 4293 datetime_format=TKS_DATE_TIME_FORMAT, 4294 mode="w", 4295 ) as writer: 4296 humanReadable = calendar.copy(deep=True) 4297 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4298 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4299 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4300 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4301 humanReadable.columns = colNames # human-readable column names 4302 4303 humanReadable.to_excel( 4304 writer, 4305 sheet_name="Bond payments calendar", 4306 index=False, 4307 encoding="UTF-8", 4308 freeze_panes=(1, 2), 4309 ) # saving as XLSX-file with freeze first row and column as headers 4310 4311 del humanReadable # release df in memory 4312 4313 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4314 4315 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4317 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4318 """ 4319 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4320 Also, creates Markdown file with calendar data, `calendar.md` by default. 4321 4322 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4323 4324 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4325 extended information about bonds: main info, current prices, bond payment calendar, 4326 coupon yields, current yields and some statistics etc. 4327 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4328 :param show: if `True` then also printing bonds payment calendar to the console, 4329 otherwise save to file `calendarFile` only. `False` by default. 4330 :return: multilines text in Markdown format with bonds payment calendar as a table. 4331 """ 4332 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4333 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4334 4335 infoText = "# Bond payments calendar\n\n" 4336 4337 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4338 4339 if not (calendar is None or calendar.empty): 4340 splitLine = "| | | | | | | | | |\n" 4341 4342 info = [ 4343 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4344 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4345 ] 4346 4347 newMonth = False 4348 notOneBond = calendar["figi"].nunique() > 1 4349 for i, bond in enumerate(calendar.iterrows()): 4350 if newMonth and notOneBond: 4351 info.append(splitLine) 4352 4353 info.append( 4354 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4355 " √" if bond[1]["paid"] else " —", 4356 bond[1]["couponDate"].split("T")[0], 4357 bond[1]["figi"], 4358 bond[1]["ticker"], 4359 bond[1]["couponNumber"], 4360 "{} {}".format( 4361 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4362 bond[1]["payCurrency"], 4363 ), 4364 bond[1]["couponType"], 4365 bond[1]["couponPeriod"], 4366 bond[1]["fixDate"].split("T")[0], 4367 ) 4368 ) 4369 4370 if i < len(calendar.values) - 1: 4371 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4372 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4373 newMonth = False if curDate.month == nextDate.month else True 4374 4375 else: 4376 newMonth = False 4377 4378 infoText += "".join(info) 4379 4380 if show: 4381 uLogger.info("{}".format(infoText)) 4382 4383 if self.calendarFile is not None: 4384 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4385 fH.write(infoText) 4386 4387 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4388 4389 else: 4390 infoText += "No data\n" 4391 4392 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4394 def OverviewAccounts(self, show: bool = False) -> dict: 4395 """ 4396 Method for parsing and show simple table with all available user accounts. 4397 4398 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4399 4400 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4401 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4402 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4403 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4404 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4405 "closed": "—", "access": "Full access" }, ...}}` 4406 """ 4407 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4408 4409 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4410 accounts = { 4411 item["id"]: { 4412 "type": TKS_ACCOUNT_TYPES[item["type"]], 4413 "name": item["name"], 4414 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4415 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4416 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4417 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4418 } for item in rawAccounts["accounts"] 4419 } 4420 4421 # Raw and parsed data with some fields replaced in "stat" section: 4422 view = { 4423 "rawAccounts": rawAccounts, 4424 "stat": accounts, 4425 } 4426 4427 # --- Prepare simple text table with only accounts data in human-readable format: 4428 if show: 4429 info = [ 4430 "# User accounts\n\n", 4431 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4432 "| Account ID | Type | Status | Name |\n", 4433 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4434 ] 4435 4436 for account in view["stat"].keys(): 4437 info.extend([ 4438 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4439 account, 4440 view["stat"][account]["type"], 4441 view["stat"][account]["status"], 4442 view["stat"][account]["name"], 4443 ) 4444 ]) 4445 4446 infoText = "".join(info) 4447 4448 uLogger.info(infoText) 4449 4450 if self.userAccountsFile: 4451 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4452 fH.write(infoText) 4453 4454 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4455 4456 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4458 def OverviewUserInfo(self, show: bool = False) -> dict: 4459 """ 4460 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4461 4462 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4463 4464 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4465 :return: dict with raw parsed data from server and some calculated statistics about it. 4466 """ 4467 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4468 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4469 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4470 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4471 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4472 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4473 4474 # This is dict with parsed common user data: 4475 userInfo = { 4476 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4477 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4478 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4479 "tariff": rawUserInfo["tariff"], 4480 } 4481 4482 # This is an array of dict with parsed margin statuses for every account IDs: 4483 margins = {} 4484 for accountId in accounts.keys(): 4485 if rawMargins[accountId]: 4486 margins[accountId] = { 4487 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4488 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4489 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4490 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4491 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4492 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4493 } 4494 4495 else: 4496 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4497 4498 unary = {} # unary-connection limits 4499 for item in rawTariffLimits["unaryLimits"]: 4500 if item["limitPerMinute"] in unary.keys(): 4501 unary[item["limitPerMinute"]].extend(item["methods"]) 4502 4503 else: 4504 unary[item["limitPerMinute"]] = item["methods"] 4505 4506 stream = {} # stream-connection limits 4507 for item in rawTariffLimits["streamLimits"]: 4508 if item["limit"] in stream.keys(): 4509 stream[item["limit"]].extend(item["streams"]) 4510 4511 else: 4512 stream[item["limit"]] = item["streams"] 4513 4514 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4515 limits = { 4516 "unary": unary, 4517 "stream": stream, 4518 } 4519 4520 # Raw and parsed data as an output result: 4521 view = { 4522 "rawUserInfo": rawUserInfo, 4523 "rawAccounts": rawAccounts, 4524 "rawMargins": rawMargins, 4525 "rawTariffLimits": rawTariffLimits, 4526 "stat": { 4527 "userInfo": userInfo, 4528 "accounts": accounts, 4529 "margins": margins, 4530 "limits": limits, 4531 }, 4532 } 4533 4534 # --- Prepare text table with user information in human-readable format: 4535 if show: 4536 info = [ 4537 "# Full user information\n\n", 4538 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4539 "## Common information\n\n", 4540 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4541 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4542 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4543 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4544 "\n## User accounts\n\n", 4545 ] 4546 4547 for account in view["stat"]["accounts"].keys(): 4548 info.extend([ 4549 "### ID: [{}]\n\n".format(account), 4550 "| Parameters | Values |\n", 4551 "|----------------------|--------------------------------------------------------------|\n", 4552 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4553 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4554 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4555 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4556 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4557 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4558 ]) 4559 4560 if margins[account]: 4561 info.extend([ 4562 "| Margin status: | Enabled |\n", 4563 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4564 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4565 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4566 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4567 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4568 ]) 4569 4570 else: 4571 info.append("| Margin status: | Disabled |\n\n") 4572 4573 info.extend([ 4574 "\n## Current user tariff limits\n", 4575 "\nSee also:\n", 4576 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4577 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4578 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4579 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4580 "\n### Unary limits\n", 4581 ]) 4582 4583 if unary: 4584 for key, values in sorted(unary.items()): 4585 info.append("\n* Max requests per minute: {}\n".format(key)) 4586 4587 for value in values: 4588 info.append(" - {}\n".format(value)) 4589 4590 else: 4591 info.append("\nNot available\n") 4592 4593 info.append("\n### Stream limits\n") 4594 4595 if stream: 4596 for key, values in sorted(stream.items()): 4597 info.append("\n* Max stream connections: {}\n".format(key)) 4598 4599 for value in values: 4600 info.append(" - {}\n".format(value)) 4601 4602 else: 4603 info.append("\nNot available\n") 4604 4605 infoText = "".join(info) 4606 4607 uLogger.info(infoText) 4608 4609 if self.userInfoFile: 4610 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4611 fH.write(infoText) 4612 4613 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4614 4615 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4618class Args: 4619 """ 4620 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4621 """ 4622 def __init__(self, **kwargs): 4623 self.__dict__.update(kwargs) 4624 4625 def __getattr__(self, item): 4626 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4629def ParseArgs(): 4630 """This function get and parse command line keys.""" 4631 parser = ArgumentParser() # command-line string parser 4632 4633 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4634 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4635 4636 # --- options: 4637 4638 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4639 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4640 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4641 4642 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4643 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4644 4645 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4646 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4647 4648 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4649 4650 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4651 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4652 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4653 4654 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4655 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4656 4657 # --- commands: 4658 4659 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4660 4661 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4662 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4663 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4664 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4665 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4666 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4667 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4668 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4669 4670 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4671 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4672 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4673 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4674 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4675 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4676 4677 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4678 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4679 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4680 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4681 4682 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4683 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4684 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4685 4686 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4687 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4688 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4689 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4690 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4691 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4692 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4693 4694 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4695 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4696 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4697 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4698 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4699 4700 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4701 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4702 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4703 4704 cmdArgs = parser.parse_args() 4705 return cmdArgs
This function get and parse command line keys.
4708def Main(**kwargs): 4709 """ 4710 Main function for work with TKSBrokerAPI in the console. 4711 4712 See examples: 4713 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4714 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4715 """ 4716 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4717 4718 if args.debug_level: 4719 uLogger.level = 10 # always debug level by default 4720 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4721 4722 exitCode = 0 4723 start = datetime.now(tzutc()) 4724 uLogger.debug("=-" * 50) 4725 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4726 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4727 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4728 )) 4729 4730 # trying to calculate full current version: 4731 buildVersion = __version__ 4732 try: 4733 v = version("tksbrokerapi") 4734 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4735 4736 except Exception: 4737 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4738 4739 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4740 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4741 4742 try: 4743 if args.version: 4744 print("TKSBrokerAPI {}".format(buildVersion)) 4745 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4746 4747 else: 4748 # Init class for trading with Tinkoff Broker: 4749 trader = TinkoffBrokerServer( 4750 token=args.token, 4751 accountId=args.account_id, 4752 useCache=not args.no_cache, 4753 ) 4754 4755 # --- set some options: 4756 4757 if args.more: 4758 trader.moreDebug = True 4759 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4760 4761 if args.ticker: 4762 ticker = str(args.ticker).upper() # Tickers may be upper case only 4763 4764 if ticker in trader.aliasesKeys: 4765 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4766 4767 else: 4768 trader.ticker = ticker 4769 4770 if args.figi: 4771 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4772 4773 if args.depth is not None: 4774 trader.depth = args.depth 4775 4776 # --- do one command: 4777 4778 if args.list: 4779 if args.output is not None: 4780 trader.instrumentsFile = args.output 4781 4782 trader.ShowInstrumentsInfo(show=True) 4783 4784 elif args.list_xlsx: 4785 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4786 4787 elif args.bonds_xlsx is not None: 4788 if args.output is not None: 4789 trader.bondsXLSXFile = args.output 4790 4791 if len(args.bonds_xlsx) == 0: 4792 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4793 4794 else: 4795 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4796 4797 elif args.search: 4798 if args.output is not None: 4799 trader.searchResultsFile = args.output 4800 4801 trader.SearchInstruments(pattern=args.search[0], show=True) 4802 4803 elif args.info: 4804 if not (args.ticker or args.figi): 4805 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4806 raise Exception("Ticker or FIGI required") 4807 4808 if args.output is not None: 4809 trader.infoFile = args.output 4810 4811 if args.ticker: 4812 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4813 4814 else: 4815 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4816 4817 elif args.calendar is not None: 4818 if args.output is not None: 4819 trader.calendarFile = args.output 4820 4821 if len(args.calendar) == 0: 4822 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4823 4824 else: 4825 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4826 4827 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4828 4829 elif args.price: 4830 if not (args.ticker or args.figi): 4831 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4832 raise Exception("Ticker or FIGI required") 4833 4834 trader.GetCurrentPrices(show=True) 4835 4836 elif args.prices is not None: 4837 if args.output is not None: 4838 trader.pricesFile = args.output 4839 4840 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4841 4842 elif args.overview: 4843 if args.output is not None: 4844 trader.overviewFile = args.output 4845 4846 trader.Overview(show=True, details="full") 4847 4848 elif args.overview_digest: 4849 if args.output is not None: 4850 trader.overviewDigestFile = args.output 4851 4852 trader.Overview(show=True, details="digest") 4853 4854 elif args.overview_positions: 4855 if args.output is not None: 4856 trader.overviewPositionsFile = args.output 4857 4858 trader.Overview(show=True, details="positions") 4859 4860 elif args.overview_orders: 4861 if args.output is not None: 4862 trader.overviewOrdersFile = args.output 4863 4864 trader.Overview(show=True, details="orders") 4865 4866 elif args.overview_analytics: 4867 if args.output is not None: 4868 trader.overviewAnalyticsFile = args.output 4869 4870 trader.Overview(show=True, details="analytics") 4871 4872 elif args.overview_calendar: 4873 if args.output is not None: 4874 trader.overviewAnalyticsFile = args.output 4875 4876 trader.Overview(show=True, details="calendar") 4877 4878 elif args.deals is not None: 4879 if args.output is not None: 4880 trader.reportFile = args.output 4881 4882 if 0 <= len(args.deals) < 3: 4883 trader.Deals( 4884 start=args.deals[0] if len(args.deals) >= 1 else None, 4885 end=args.deals[1] if len(args.deals) == 2 else None, 4886 show=True, # Always show deals report in console 4887 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4888 ) 4889 4890 else: 4891 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4892 raise Exception("Incorrect value") 4893 4894 elif args.history is not None: 4895 if args.output is not None: 4896 trader.historyFile = args.output 4897 4898 if 0 <= len(args.history) < 3: 4899 dataReceived = trader.History( 4900 start=args.history[0] if len(args.history) >= 1 else None, 4901 end=args.history[1] if len(args.history) == 2 else None, 4902 interval="hour" if args.interval is None or not args.interval else args.interval, 4903 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4904 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4905 show=True, # shows all downloaded candles in console 4906 ) 4907 4908 if args.render_chart is not None and dataReceived is not None: 4909 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4910 4911 trader.ShowHistoryChart( 4912 candles=dataReceived, 4913 interact=iChart, 4914 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4915 ) 4916 4917 else: 4918 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4919 raise Exception("Incorrect value") 4920 4921 elif args.load_history is not None: 4922 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4923 4924 if args.render_chart is not None and histData is not None: 4925 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4926 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4927 4928 trader.ShowHistoryChart( 4929 candles=histData, 4930 interact=iChart, 4931 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4932 ) 4933 4934 elif args.trade is not None: 4935 if 1 <= len(args.trade) <= 5: 4936 trader.Trade( 4937 operation=args.trade[0], 4938 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4939 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4940 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4941 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4942 ) 4943 4944 else: 4945 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4946 4947 elif args.buy is not None: 4948 if 0 <= len(args.buy) <= 4: 4949 trader.Buy( 4950 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4951 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4952 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4953 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4954 ) 4955 4956 else: 4957 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4958 4959 elif args.sell is not None: 4960 if 0 <= len(args.sell) <= 4: 4961 trader.Sell( 4962 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4963 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4964 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4965 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4966 ) 4967 4968 else: 4969 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4970 4971 elif args.order: 4972 if 4 <= len(args.order) <= 7: 4973 trader.Order( 4974 operation=args.order[0], 4975 orderType=args.order[1], 4976 lots=int(args.order[2]), 4977 targetPrice=float(args.order[3]), 4978 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4979 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4980 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4981 ) 4982 4983 else: 4984 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4985 4986 elif args.buy_limit: 4987 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4988 4989 elif args.sell_limit: 4990 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4991 4992 elif args.buy_stop: 4993 if 2 <= len(args.buy_stop) <= 7: 4994 trader.BuyStop( 4995 lots=int(args.buy_stop[0]), 4996 targetPrice=float(args.buy_stop[1]), 4997 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4998 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4999 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5000 ) 5001 5002 else: 5003 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5004 5005 elif args.sell_stop: 5006 if 2 <= len(args.sell_stop) <= 7: 5007 trader.SellStop( 5008 lots=int(args.sell_stop[0]), 5009 targetPrice=float(args.sell_stop[1]), 5010 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5011 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5012 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5013 ) 5014 5015 else: 5016 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5017 5018 # elif args.buy_order_grid is not None: 5019 # # update order grid work with api v2 5020 # if len(args.buy_order_grid) == 2: 5021 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5022 # 5023 # for order in orderParams: 5024 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5025 # 5026 # else: 5027 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5028 # 5029 # elif args.sell_order_grid is not None: 5030 # # update order grid work with api v2 5031 # if len(args.sell_order_grid) >= 2: 5032 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5033 # 5034 # for order in orderParams: 5035 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5036 # 5037 # else: 5038 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5039 5040 elif args.close_order is not None: 5041 trader.CloseOrders(args.close_order) # close only one order 5042 5043 elif args.close_orders is not None: 5044 trader.CloseOrders(args.close_orders) # close list of orders 5045 5046 elif args.close_trade: 5047 if not (args.ticker or args.figi): 5048 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5049 raise Exception("Ticker or FIGI required") 5050 5051 if args.ticker: 5052 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5053 5054 else: 5055 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5056 5057 elif args.close_trades is not None: 5058 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5059 5060 elif args.close_all is not None: 5061 if args.ticker: 5062 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5063 5064 elif args.figi: 5065 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5066 5067 else: 5068 trader.CloseAll(*args.close_all) 5069 5070 elif args.limits: 5071 if args.output is not None: 5072 trader.withdrawalLimitsFile = args.output 5073 5074 trader.OverviewLimits(show=True) 5075 5076 elif args.user_info: 5077 if args.output is not None: 5078 trader.userInfoFile = args.output 5079 5080 trader.OverviewUserInfo(show=True) 5081 5082 elif args.account: 5083 if args.output is not None: 5084 trader.userAccountsFile = args.output 5085 5086 trader.OverviewAccounts(show=True) 5087 5088 else: 5089 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5090 raise Exception("There is no command to execute") 5091 5092 except Exception: 5093 trace = tb.format_exc() 5094 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5095 if e in trace: 5096 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5097 break 5098 5099 uLogger.debug(trace) 5100 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5101 exitCode = 255 # an error occurred, must be open a ticket for this issue 5102 5103 finally: 5104 finish = datetime.now(tzutc()) 5105 5106 if exitCode == 0: 5107 if args.more: 5108 uLogger.debug("All operations were finished success (summary code is 0).") 5109 5110 else: 5111 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5112 os.path.abspath(uLog.defaultLogFile), exitCode, 5113 )) 5114 5115 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5116 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5117 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5118 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5119 )) 5120 uLogger.debug("=-" * 50) 5121 5122 if not kwargs: 5123 sys.exit(exitCode) 5124 5125 else: 5126 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: